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.
|
# 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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
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"
|
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"
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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 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>) {}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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"
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 app;
|
||||||
|
|
||||||
mod egl;
|
|
||||||
mod rfb;
|
mod rfb;
|
||||||
|
|
||||||
use app::*;
|
use app::*;
|
||||||
|
|
Loading…
Reference in a new issue