update to latest retro_frontend
This commit is contained in:
parent
b31feab847
commit
a7a26218b0
23 changed files with 898 additions and 428 deletions
19
Cargo.lock
generated
19
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -3,3 +3,8 @@ resolver = "2"
|
|||
members = [
|
||||
"crates/*"
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
gl = "0.14.0"
|
||||
gl_generator = "0.14.0"
|
||||
thiserror = "1.0.61"
|
||||
|
|
12
crates/letsplay_gpu/Cargo.toml
Normal file
12
crates/letsplay_gpu/Cargo.toml
Normal 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
|
7
crates/letsplay_gpu/README.md
Normal file
7
crates/letsplay_gpu/README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# `letsplay_gpu`
|
||||
|
||||
Shared helpers for Let's Play GPU stuff.
|
||||
|
||||
- EGL bindings
|
||||
- GL FBO helpers
|
||||
- ...
|
28
crates/letsplay_gpu/build.rs
Normal file
28
crates/letsplay_gpu/build.rs
Normal 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();
|
||||
}
|
161
crates/letsplay_gpu/src/gl_framebuffer.rs
Normal file
161
crates/letsplay_gpu/src/gl_framebuffer.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
216
crates/letsplay_gpu/src/lib.rs
Normal file
216
crates/letsplay_gpu/src/lib.rs
Normal 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::*;
|
|
@ -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"
|
||||
|
|
|
@ -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<HwGlInitData>;
|
||||
}
|
||||
|
||||
/// 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<u32>,
|
||||
pub(crate) converted_pixel_buffer: Option<Box<[u32]>>,
|
||||
|
||||
// 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<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 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();
|
||||
(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
115
crates/retro_frontend/src/input_devices/analog_retropad.rs
Normal file
115
crates/retro_frontend/src/input_devices/analog_retropad.rs
Normal 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,
|
||||
_ => {}
|
||||
},
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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>) {}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
self.buttons[id as usize]
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
for button in &mut self.buttons {
|
||||
*button = 0i16;
|
||||
for i in 0..self.buttons.len() {
|
||||
if self.buttons[i] != 0 {
|
||||
mask |= 1 << i;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
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 {
|
||||
(*(*FRONTEND).interface).video_resize(width, height);
|
||||
}
|
||||
|
||||
// bleh
|
||||
(*FRONTEND).fb_width = width;
|
||||
(*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) {
|
||||
(*(*FRONTEND).interface).video_update_gl();
|
||||
return;
|
||||
|
@ -232,44 +245,104 @@ 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() {
|
||||
(*(*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(
|
||||
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,
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,7 +8,6 @@ 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.
|
||||
///
|
||||
|
@ -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,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)]
|
||||
#[macro_export]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<gpu::egl_helpers::DeviceContext>,
|
||||
|
||||
/// 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<u32>,
|
||||
|
@ -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<HwGlInitData> {
|
||||
// test SW-only even if a core wants to upgrade
|
||||
//return None;
|
||||
|
||||
// Initalize EGL
|
||||
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);
|
||||
|
||||
// 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");
|
||||
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.
|
||||
unsafe {
|
||||
gl::load_with(|s| {
|
||||
let str = std::ffi::CString::new(s).expect("Uhh huh.");
|
||||
std::mem::transmute(egl::GetProcAddress(str.as_ptr()))
|
||||
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());
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
|
@ -8,7 +8,6 @@ use clap::{arg, command, value_parser};
|
|||
|
||||
mod app;
|
||||
|
||||
mod egl;
|
||||
mod rfb;
|
||||
|
||||
use app::*;
|
||||
|
|
Loading…
Reference in a new issue