update to latest retro_frontend

This commit is contained in:
Lily Tsuru 2025-01-06 15:21:48 -05:00
parent b31feab847
commit a7a26218b0
23 changed files with 898 additions and 428 deletions

19
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
@ -275,6 +275,14 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "letsplay_gpu"
version = "0.1.0"
dependencies = [
"gl",
"gl_generator",
]
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.155" version = "0.2.155"
@ -445,7 +453,6 @@ dependencies = [
"libc", "libc",
"libloading", "libloading",
"libretro-sys", "libretro-sys",
"rgb565",
"serde", "serde",
"thiserror", "thiserror",
"toml", "toml",
@ -460,19 +467,13 @@ dependencies = [
"cc", "cc",
"clap", "clap",
"gl", "gl",
"gl_generator", "letsplay_gpu",
"libvnc-sys", "libvnc-sys",
"retro_frontend", "retro_frontend",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
[[package]]
name = "rgb565"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43e85498d0bb728f77a88b4313eaf4ed21673f3f8a05c36e835cf6c9c0d066"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"

View file

@ -3,3 +3,8 @@ resolver = "2"
members = [ members = [
"crates/*" "crates/*"
] ]
[workspace.dependencies]
gl = "0.14.0"
gl_generator = "0.14.0"
thiserror = "1.0.61"

View file

@ -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

View file

@ -0,0 +1,7 @@
# `letsplay_gpu`
Shared helpers for Let's Play GPU stuff.
- EGL bindings
- GL FBO helpers
- ...

View file

@ -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();
}

View file

@ -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,
);
}
}
}

View file

@ -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<String> {
// 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::*;

View file

@ -9,9 +9,8 @@ edition = "2021"
libc = "0.2.155" libc = "0.2.155"
libloading = "0.8.3" libloading = "0.8.3"
libretro-sys = "0.1.1" libretro-sys = "0.1.1"
rgb565 = "0.1.3"
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
thiserror = "1.0.61" thiserror.workspace = true
toml = "0.8.19" toml = "0.8.19"
tracing = "0.1.40" tracing = "0.1.40"

View file

@ -21,12 +21,10 @@ use tracing::{error, info};
/// Only one instance of Frontend can be active in an application. /// Only one instance of Frontend can be active in an application.
pub(crate) static mut FRONTEND: *mut Frontend = std::ptr::null_mut(); 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. /// Initalization data for HW OpenGL cores.
pub struct HwGlInitData { 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. /// Interface for the frontend to call to user code.
@ -48,7 +46,9 @@ pub trait FrontendInterface {
fn input_poll(&mut self); fn input_poll(&mut self);
/// Initalize hardware accelerated rendering using OpenGL. /// 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<HwGlInitData>;
} }
/// Per-core settings /// Per-core settings
@ -75,7 +75,7 @@ pub struct Frontend {
pub(crate) pixel_format: PixelFormat, pub(crate) pixel_format: PixelFormat,
/// Converted pixel buffer. We store it here so we don't keep allocating over and over. /// Converted pixel buffer. We store it here so we don't keep allocating over and over.
pub(crate) converted_pixel_buffer: Vec<u32>, pub(crate) converted_pixel_buffer: Option<Box<[u32]>>,
// Framebuffer attributes. TODO: This really should be another struct or something // Framebuffer attributes. TODO: This really should be another struct or something
// with members to make dealing with it less annoying. // with members to make dealing with it less annoying.
@ -118,7 +118,7 @@ impl Frontend {
sys_info: None, sys_info: None,
pixel_format: PixelFormat::RGB565, pixel_format: PixelFormat::RGB565,
converted_pixel_buffer: Vec::new(), converted_pixel_buffer: None,
fb_width: 0, fb_width: 0,
fb_height: 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<String> {
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 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<SystemInfo> = MaybeUninit::uninit(); let c_name = ffi::CStr::from_ptr(system_info.library_name);
(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);
format!( format!(
"{}/{}.toml", "{}/{}.toml",
@ -202,13 +206,15 @@ impl Frontend {
) )
}; };
path Ok(path)
} }
// TODO: make this a bit less janky (and use Results) // TODO: make this a bit less janky (and use Results)
pub fn load_settings(&mut self) { 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(); let path: &Path = path_string.as_ref();
match path.try_exists() { match path.try_exists() {
@ -230,7 +236,9 @@ impl Frontend {
} }
pub fn save_settings(&mut self) { 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 { let settings = CoreSettingsFile {
variables: self.variables.clone(), variables: self.variables.clone(),
@ -327,6 +335,7 @@ impl Frontend {
(core_api_ref.retro_set_audio_sample_batch)( (core_api_ref.retro_set_audio_sample_batch)(
libretro_callbacks::audio_sample_batch_callback, 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()); info!("Core {} loaded", path.as_ref().display());
} }
@ -364,6 +373,7 @@ impl Frontend {
self.fb_width = 0; self.fb_width = 0;
self.fb_height = 0; self.fb_height = 0;
self.fb_pitch = 0; self.fb_pitch = 0;
self.converted_pixel_buffer = None;
// disconnect all currently connected joypads // disconnect all currently connected joypads
self.input_devices.clear(); self.input_devices.clear();
@ -508,13 +518,15 @@ impl Frontend {
impl Drop for Frontend { impl Drop for Frontend {
fn drop(&mut self) { fn drop(&mut self) {
if self.core_loaded() { // Null out the global frontend pointer first,
let _ = self.unload_core(); // so any attempted UAF will instead result in a segfault
}
unsafe { unsafe {
assert!(!FRONTEND.is_null()); assert!(!FRONTEND.is_null());
FRONTEND = std::ptr::null_mut(); FRONTEND = std::ptr::null_mut();
} }
if self.core_loaded() {
let _ = self.unload_core();
}
} }
} }

View file

@ -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<i16>) {
// FIXME: "press" axes.
self.pad.press_button_friend(id, pressure);
}
fn press_analog_axis(&mut self, index: u32, id: u32, pressure: Option<i16>) {
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,
_ => {}
},
_ => {}
}
}
}

View file

@ -5,18 +5,43 @@ pub use retropad::*;
pub mod mouse; pub mod mouse;
pub use 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 { pub trait InputDevice {
/// Gets the device type. This should never EVER change, and simply return a constant. /// Gets the device type. This should never EVER change, and simply return a constant.
fn device_type(&self) -> u32; 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. /// Gets the state of one button/axis.
/// is_pressed(id) can simply be expressed as `(get_button(id) != 0)`. /// is_pressed(index, id) can simply be expressed in a digital way as `(get_index(index, id) != 0)`.
fn get_button(&self, id: u32) -> i16; /// 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. /// Clears the state of all buttons/axes.
fn reset(&mut self); 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<i16>); fn press_button(&mut self, id: u32, pressure: Option<i16>);
/// 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<i16>) {}
} }

View file

@ -5,7 +5,6 @@ use crate::libretro_sys_new;
/// Implementation of the [InputDevice] trait for the Libretro mouse. /// Implementation of the [InputDevice] trait for the Libretro mouse.
pub struct Mouse { pub struct Mouse {
buttons: [i16; 8], buttons: [i16; 8],
// TODO: hold the last x/y so we can calculate relative position // 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 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 { if id > 8 {
return 0; return 0;
} }

View file

@ -13,28 +13,20 @@ impl RetroPad {
pub fn new() -> Self { pub fn new() -> Self {
Self { buttons: [0; 16] } Self { buttons: [0; 16] }
} }
}
impl InputDevice for RetroPad { pub(crate) fn button_mask(&self) -> i16 {
fn device_type(&self) -> u32 { let mut mask = 0u16;
libretro_sys_new::DEVICE_JOYPAD
}
fn get_button(&self, id: u32) -> i16 { for i in 0..self.buttons.len() {
if id > 16 { if self.buttons[i] != 0 {
return 0; mask |= 1 << i;
}
self.buttons[id as usize]
}
fn reset(&mut self) {
for button in &mut self.buttons {
*button = 0i16;
} }
} }
fn press_button(&mut self, id: u32, pressure: Option<i16>) { mask as i16
}
pub(crate) fn press_button_friend(&mut self, id: u32, pressure: Option<i16>) {
if id > 16 { if id > 16 {
return; 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<i16>) {
// Pressure is ignored and treated as binary.
self.press_button_friend(id, Some(1));
}
}

View file

@ -1,9 +1,8 @@
//! Callbacks for libretro //! Callbacks for libretro
use crate::input_devices::InputDevice;
use crate::{frontend::*, libretro_log, util}; use crate::{frontend::*, libretro_log, util};
use crate::{libretro_core_variable, libretro_sys_new::*}; use crate::{libretro_core_variable, libretro_sys_new::*};
use rgb565::Rgb565;
use std::ffi; use std::ffi;
use tracing::{debug, error}; use tracing::{debug, error};
@ -110,26 +109,40 @@ pub(crate) unsafe extern "C" fn environment_callback(
let hw_render_context_type = let hw_render_context_type =
HwContextType::from_uint(hw_render.context_type).expect("Uh oh!"); HwContextType::from_uint(hw_render.context_type).expect("Uh oh!");
if hw_render_context_type != HwContextType::OpenGL match hw_render_context_type {
&& hw_render_context_type != HwContextType::OpenGLCore 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!( error!(
"Core is trying to request an context type we don't support ({:?}), failing", "Core is trying to request an context type we don't support ({:?}), failing",
hw_render_context_type hw_render_context_type
); );
return false; return false;
} }
}
let init_data = (*(*FRONTEND).interface).hw_gl_init(); // Once we have initalized HW rendering, we do not need a conversion buffer.
(*FRONTEND).converted_pixel_buffer = None;
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();
return true; return true;
} }
@ -206,22 +219,22 @@ pub(crate) unsafe extern "C" fn video_refresh_callback(
height: ffi::c_uint, height: ffi::c_uint,
pitch: usize, pitch: usize,
) { ) {
// I guess this must be how duplicated frames are signaled. // This is how frame duplication is signaled, even w/ HW rendering (or some cores are just badly written.
// one word: Bleh // I mean I've already had to baby cores enough!).
// One word: Bleh.
if pixels.is_null() { if pixels.is_null() {
return; return;
} }
//info!("Video refresh called, {width}, {height}, {pitch}");
if (*FRONTEND).fb_width != width || (*FRONTEND).fb_height != height { if (*FRONTEND).fb_width != width || (*FRONTEND).fb_height != height {
(*(*FRONTEND).interface).video_resize(width, height);
}
// bleh // bleh
(*FRONTEND).fb_width = width; (*FRONTEND).fb_width = width;
(*FRONTEND).fb_height = height; (*FRONTEND).fb_height = height;
(*(*FRONTEND).interface).video_resize(width, 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) { if pixels == (-1i64 as *const ffi::c_void) {
(*(*FRONTEND).interface).video_update_gl(); (*(*FRONTEND).interface).video_update_gl();
return; return;
@ -232,44 +245,104 @@ pub(crate) unsafe extern "C" fn video_refresh_callback(
let pitch = (*FRONTEND).fb_pitch as usize; 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 { 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 => { PixelFormat::RGB565 => {
let pixel_data_slice = std::slice::from_raw_parts( let pixel_data_slice = std::slice::from_raw_parts(
pixels as *const u16, pixels as *const u16,
(pitch * height as usize) as usize, (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 x in 0..pitch as usize {
for y in 0..height as usize { for y in 0..height as usize {
let rgb = Rgb565::from_rgb565(pixel_data_slice[y * pitch as usize + x]); let pixel = pixel_data_slice[y * pitch as usize + x];
let comp = rgb.to_rgb888_components(); 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 buffer[y * pitch as usize + x] = (255u32 << 24)
(*FRONTEND).converted_pixel_buffer[y * pitch as usize + x] = | ((comp.2 as u32) << 16)
((comp[0] as u32) << 16) | ((comp[1] as u32) << 8) | (comp[2] as u32); | ((comp.1 as u32) << 8) | (comp.0 as u32);
}
} }
} }
(*(*FRONTEND).interface) PixelFormat::ARGB8888 => {
.video_update(&(*FRONTEND).converted_pixel_buffer[..], pitch as u32);
}
_ => {
let pixel_data_slice = std::slice::from_raw_parts( let pixel_data_slice = std::slice::from_raw_parts(
pixels as *const u32, pixels as *const u32,
(pitch * height as usize) as usize, (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() { pub(crate) unsafe extern "C" fn input_poll_callback() {
(*(*FRONTEND).interface).input_poll(); (*(*FRONTEND).interface).input_poll();
@ -278,23 +351,27 @@ pub(crate) unsafe extern "C" fn input_poll_callback() {
pub(crate) unsafe extern "C" fn input_state_callback( pub(crate) unsafe extern "C" fn input_state_callback(
port: ffi::c_uint, port: ffi::c_uint,
device: ffi::c_uint, device: ffi::c_uint,
_index: ffi::c_uint, // not used? index: ffi::c_uint, // not used?
button_id: ffi::c_uint, button_id: ffi::c_uint,
) -> ffi::c_short { ) -> ffi::c_short {
if (*FRONTEND).input_devices.contains_key(&port) { if (*FRONTEND).input_devices.contains_key(&port) {
let joypad = *(*FRONTEND) let joypad: &dyn InputDevice = &*(*(*FRONTEND).input_devices.get(&port).expect(
.input_devices "How do we get here when contains_key() returns true but the key doen't exist",
.get(&port) ));
.expect("How do we get here when contains_key() returns true but the key doen't exist");
if device == (*joypad).device_type() { if joypad.device_type_compatible(device) {
return (*joypad).get_button(button_id); return (*joypad).get_index(index, button_id);
} }
} }
0 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( pub(crate) unsafe extern "C" fn audio_sample_batch_callback(
// Is actually a [[l, r]] interleaved pair. // Is actually a [[l, r]] interleaved pair.
samples: *const i16, samples: *const i16,

View file

@ -20,7 +20,6 @@ pub struct CoreVariable {
} }
impl CoreVariable { impl CoreVariable {
/// Parses this core variable. /// Parses this core variable.
pub fn parse(str: &str) -> Self { pub fn parse(str: &str) -> Self {
let string = str.to_string(); let string = str.to_string();
@ -29,6 +28,7 @@ impl CoreVariable {
Some(index) => { Some(index) => {
let name = &string[0..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() != ' ' { if string.chars().nth(index + 1).unwrap() != ' ' {
panic!("Improperly formatted core variable"); panic!("Improperly formatted core variable");
} }

View file

@ -6,9 +6,12 @@ use tracing::*;
#[no_mangle] #[no_mangle]
/// This recieves log messages from our C++ helper code, and pulls them out into Tracing messages. /// 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) { 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. // we really only should get UTF-8 errors here in the case a core spits out something invalid.
unsafe { unsafe {
#[cfg(debug_assertions)]
assert!(!buf.is_null(), "This pointer should NEVER be null");
match ffi::CStr::from_ptr(buf).to_str() { match ffi::CStr::from_ptr(buf).to_str() {
Ok(message) => match level { Ok(message) => match level {
LogLevel::Debug => { LogLevel::Debug => {

View file

@ -3,6 +3,10 @@
pub use libretro_sys::*; pub use libretro_sys::*;
use std::ffi; 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. /// Defines overrides which modify frontend handling of specific content file types.
/// An array of [SystemContentInfoOverride] is passed to [RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE] /// An array of [SystemContentInfoOverride] is passed to [RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE]
#[repr(C)] #[repr(C)]

View file

@ -1,4 +1,5 @@
use crate::libretro_sys_new::*; use crate::libretro_sys_new::*;
use std::alloc;
pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 { pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 {
match pf { match pf {
@ -7,7 +8,6 @@ pub fn bytes_per_pixel_from_libretro(pf: PixelFormat) -> u32 {
} }
} }
/// Boilerplate code for dealing with NULL/otherwise terminated arrays, /// Boilerplate code for dealing with NULL/otherwise terminated arrays,
/// which converts them into a Rust slice. /// which converts them into a Rust slice.
/// ///
@ -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 // 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 // *before* this is called by the user, but to avoid anything going haywire
// we additionally check here. // 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 { unsafe {
let mut iter = ptr.clone(); let mut iter = ptr.clone();
@ -39,6 +42,20 @@ 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<T: Sized>(len: usize) -> Box<[T]> {
assert_ne!(len, 0, "length cannot be 0");
let layout = alloc::Layout::array::<T>(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)] #[doc(hidden)]
#[macro_export] #[macro_export]

View file

@ -7,12 +7,12 @@ publish = false
[dependencies] [dependencies]
anyhow = "1.0.86" anyhow = "1.0.86"
clap = { version = "4.5.6", features = ["cargo"] } clap = { version = "4.5.6", features = ["cargo"] }
gl = "0.14.0" gl.workspace = true
libvnc-sys = "0.1.4" libvnc-sys = "0.1.4"
retro_frontend = { path = "../retro_frontend" } retro_frontend = { path = "../retro_frontend" }
letsplay_gpu = { path = "../letsplay_gpu" }
tracing = "0.1.40" tracing = "0.1.40"
tracing-subscriber = "0.3.18" tracing-subscriber = "0.3.18"
[build-dependencies] [build-dependencies]
cc = "1.0.99" cc = "1.0.99"
gl_generator = "0.14.0"

View file

@ -1,31 +1,6 @@
use cc; use cc;
use gl_generator::{Api, Fallbacks, Profile, Registry, StaticGenerator};
use std::env;
use std::fs::File;
use std::path::Path;
fn main() { 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++ // C++
let mut build = cc::Build::new(); let mut build = cc::Build::new();
build build

View file

@ -1,15 +1,14 @@
use crate::egl;
use crate::rfb::*; use crate::rfb::*;
use std::{path::Path, time::Duration}; use std::{path::Path, time::Duration};
use std::ptr::{addr_of_mut, null};
use retro_frontend::{ use retro_frontend::{
frontend::{Frontend, FrontendInterface, HwGlInitData}, frontend::{Frontend, FrontendInterface, HwGlInitData},
input_devices::{InputDevice, RetroPad}, input_devices::{InputDevice, RetroPad},
}; };
use letsplay_gpu as gpu;
use anyhow::Result; use anyhow::Result;
/// Called by OpenGL. We use this to dump errors. /// Called by OpenGL. We use this to dump errors.
@ -45,17 +44,13 @@ pub struct App {
pad: RetroPad, pad: RetroPad,
// EGL state
egl_context: Option<gpu::egl_helpers::DeviceContext>,
/// True if HW rendering is active. /// True if HW rendering is active.
using_hardware_rendering: bool, using_hardware_rendering: bool,
// EGL state gl_framebuffer: gpu::GlFramebuffer,
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. /// Cached readback buffer.
readback_buffer: Vec<u32>, readback_buffer: Vec<u32>,
@ -67,12 +62,9 @@ impl App {
frontend: None, frontend: None,
rfb_server: RfbServer::new(rfb_config)?, rfb_server: RfbServer::new(rfb_config)?,
pad: RetroPad::new(), pad: RetroPad::new(),
egl_context: None,
using_hardware_rendering: false, using_hardware_rendering: false,
egl_display: null(), gl_framebuffer: gpu::GlFramebuffer::new(),
egl_context: null(),
texture_id: 0,
renderbuffer_id: 0,
fbo_id: 0,
readback_buffer: Vec::new(), readback_buffer: Vec::new(),
}); });
@ -137,172 +129,36 @@ impl App {
Ok(()) 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) { fn hw_gl_egl_init(&mut self) {
// Currently we assume the first device on the Device platform. // expected
// In most cases (at least on NVIDIA), this is usually a real GPU. self.egl_context = Some(gpu::egl_helpers::DeviceContext::new(0));
self.egl_display = egl::get_device_platform_display(0); self.egl_context.as_mut().unwrap().make_current();
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
};
} }
/// Destroys EGL resources. /// Destroys OpenGL resources and the EGL context.
fn hw_gl_egl_exit(&mut self) { fn hw_gl_destroy(&mut self) {
if self.using_hardware_rendering { self.gl_framebuffer.destroy();
// Delete FBO
self.hw_gl_delete_fbo();
// Release the EGL context we created before destroying it {
unsafe { let m = self.egl_context.as_mut().unwrap();
egl::MakeCurrent( m.release();
self.egl_display, m.destroy();
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();
}
} }
/// Deletes all OpenGL FBO resources (the FBO itself, the render texture, and the renderbuffer used for depth) self.egl_context = None;
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);
}
} }
/// The main loop. Should probably be abstracted a bit better. /// The main loop. Should probably be abstracted a bit better.
pub fn main_loop(&mut self) -> ! { pub fn main_loop(&mut self) -> ! {
let frontend = self.get_frontend(); 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 // Do the main loop
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(); frontend.run_frame();
std::thread::sleep(step_duration); std::thread::sleep(step_duration);
} }
@ -315,7 +171,14 @@ impl FrontendInterface for App {
// Recreate the OpenGL FBO on resize. // Recreate the OpenGL FBO on resize.
if self.using_hardware_rendering { 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); 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. // 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 // SAFETY: self.readback_buffer will always be allocated to the proper size before reaching here
unsafe { unsafe {
gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id); gl::BindFramebuffer(gl::FRAMEBUFFER, self.gl_framebuffer.as_raw());
gl::ReadPixels( gl::ReadPixels(
0, 0,
@ -366,48 +229,52 @@ impl FrontendInterface for App {
} }
} }
fn hw_gl_init(&mut self) -> HwGlInitData { fn hw_gl_init(&mut self) -> Option<HwGlInitData> {
if self.using_hardware_rendering { // test SW-only even if a core wants to upgrade
panic!("Cannot initalize HW rendering while already initalized"); //return None;
}
// Initalize EGL if self.egl_context.is_none() {
self.hw_gl_egl_init(); 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 // 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()) { 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"); tracing::error!("Your graphics driver doesn't support the EGL_KHR_get_all_proc_addresses extension.");
panic!("Cannot initalize OpenGL rendering"); 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. // Load OpenGL functions using the EGL loader.
unsafe {
gl::load_with(|s| { gl::load_with(|s| {
let str = std::ffi::CString::new(s).expect("Uhh huh."); let str = std::ffi::CString::new(s).expect("gl::load_with fail");
std::mem::transmute(egl::GetProcAddress(str.as_ptr())) std::mem::transmute(gpu::egl::GetProcAddress(str.as_ptr()))
}); });
// set OpenGL debug message callback // set OpenGL debug message callback
gl::Enable(gl::DEBUG_OUTPUT); gl::Enable(gl::DEBUG_OUTPUT);
gl::DebugMessageCallback(Some(opengl_message_callback), null()); 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 // Create the initial FBO for the core to render to
let dimensions = self.get_frontend().get_size(); 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 Some(HwGlInitData {
get_proc_address: gpu::egl::GetProcAddress as *mut std::ffi::c_void,
return unsafe { });
HwGlInitData {
get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void),
}
};
} }
} }

View file

@ -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<String> {
// 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::*;

View file

@ -8,7 +8,6 @@ use clap::{arg, command, value_parser};
mod app; mod app;
mod egl;
mod rfb; mod rfb;
use app::*; use app::*;