move app code to new module

Mostly to keep it clean.
This commit is contained in:
Lily Tsuru 2024-08-06 22:08:37 -04:00
parent b377686c4b
commit 9e2324fc4a
3 changed files with 402 additions and 380 deletions

View file

@ -6,7 +6,7 @@ use rgb565::Rgb565;
use std::ffi; use std::ffi;
use tracing::{debug, error, info}; use tracing::{debug, error};
/// This function is used with HW OpenGL cores to transfer the current FBO's ID. /// This function is used with HW OpenGL cores to transfer the current FBO's ID.
unsafe extern "C" fn hw_gl_get_framebuffer() -> usize { unsafe extern "C" fn hw_gl_get_framebuffer() -> usize {
@ -110,7 +110,9 @@ 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 && hw_render_context_type != HwContextType::OpenGLCore { if hw_render_context_type != HwContextType::OpenGL
&& hw_render_context_type != HwContextType::OpenGLCore
{
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

394
crates/retrovnc/src/app.rs Normal file
View file

@ -0,0 +1,394 @@
use crate::egl;
use crate::rfb::*;
use std::{path::Path, time::Duration};
use retro_frontend::{
frontend::{Frontend, FrontendInterface, HwGlInitData},
input_devices::{InputDevice, RetroPad},
};
use anyhow::Result;
/// Called by OpenGL. We use this to dump errors.
extern "system" fn opengl_message_callback(
source: gl::types::GLenum,
_type: gl::types::GLenum,
id: gl::types::GLuint,
_severity: gl::types::GLenum,
_length: gl::types::GLsizei,
message: *const gl::types::GLchar,
_user: *mut std::ffi::c_void,
) {
unsafe {
let message = std::ffi::CStr::from_ptr(message);
if _type == gl::DEBUG_TYPE_ERROR {
tracing::error!(
"OpenGL error: {:?} (res {:08x}, id = {:08x}, source = {:08x})",
message,
_type,
id,
source
);
}
}
}
pub struct App {
/// The frontend.
frontend: Option<Box<Frontend>>,
/// VNC server
rfb_server: Box<RfbServer>,
pad: RetroPad,
/// True if HW rendering is active.
hw_render: 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,
/// Cached readback buffer.
readback_buffer: Vec<u32>,
}
impl App {
pub fn new(rfb_config: RfbServerConfig) -> Result<Box<Self>> {
let mut boxed = Box::new(Self {
frontend: None,
rfb_server: RfbServer::new(rfb_config)?,
pad: RetroPad::new(),
hw_render: false,
egl_display: std::ptr::null(),
egl_context: std::ptr::null(),
texture_id: 0,
renderbuffer_id: 0,
fbo_id: 0,
readback_buffer: Vec::new(),
});
// SAFETY: The boxed allocation will never drop since the main loop always loops forever.
// Even if it did, the only way to touch the pointer involves the frontend library calling retro_run,
// and the core calling one of the given callbacks. Therefore this is gnarly, but "fine".
//
// I'm still not really sure how to tell the borrow checker that this is alright,
// short of Box::leak() (which I don't want to do, since ideally I'd like actual cleanup to occur).
let obj = &mut *boxed as &mut dyn FrontendInterface;
boxed.frontend = Some(Frontend::new(obj as *mut dyn FrontendInterface));
Ok(boxed)
}
fn get_frontend(&mut self) -> &mut Frontend {
self.frontend.as_mut().unwrap()
}
pub fn init(&mut self) {
// Currently retrovnc just hardcodes the assumption of a single RetroPad.
// SAFETY: This too won't ever be Use-After-Free'd because the only oppoturnity to
// goes away on drop as well. That's a bit flaky reasoning wise, but is true.
//
// In all honesty, I'm not sure this even needs to be a *mut so I could see if
// making it a immutable reference works.
let pad = &mut self.pad as *mut dyn InputDevice;
self.get_frontend().plug_input_device(0, pad);
// Initalize the display
self.init_display();
}
fn init_display(&mut self) {
let av_info = self.get_frontend().get_av_info().expect("No AV info");
// Start VNC server.
{
let server = &mut self.rfb_server;
tracing::info!("Starting VNC server");
server.start();
server.resize(
av_info.geometry.base_width as u16,
av_info.geometry.base_height as u16,
);
}
}
pub fn load_core<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
// Unload an existing core.
if self.get_frontend().core_loaded() {
let _ = self.get_frontend().unload_core();
}
self.get_frontend().load_core(path)?;
Ok(())
}
pub fn load_game<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.get_frontend().load_game(path)?;
Ok(())
}
/// Initalizes a headless EGL context for OpenGL rendering.
unsafe fn hw_gl_egl_init(&mut self) {
self.egl_display = egl::get_device_platform_display();
self.egl_context = {
const EGL_CONFIG_ATTRIBUTES: [egl::types::EGLenum; 13] = [
egl::SURFACE_TYPE,
egl::PBUFFER_BIT,
egl::BLUE_SIZE,
8,
egl::RED_SIZE,
8,
egl::BLUE_SIZE,
8,
egl::DEPTH_SIZE,
8,
egl::RENDERABLE_TYPE,
egl::OPENGL_BIT,
egl::NONE,
];
let mut egl_major: egl::EGLint = 0;
let mut egl_minor: egl::EGLint = 0;
let mut egl_config_count: egl::EGLint = 0;
let mut config: egl::types::EGLConfig = std::ptr::null();
egl::Initialize(
self.egl_display,
std::ptr::addr_of_mut!(egl_major),
std::ptr::addr_of_mut!(egl_minor),
);
egl::ChooseConfig(
self.egl_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(self.egl_display, config, egl::NO_CONTEXT, std::ptr::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.
unsafe fn hw_gl_egl_exit(&mut self) {
if self.hw_render {
// Delete FBO
self.hw_gl_delete_fbo();
// Release the EGL context we created before destroying it
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 = std::ptr::null();
self.egl_context = std::ptr::null();
}
}
/// 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, std::ptr::addr_of_mut!(self.fbo_id));
self.fbo_id = 0;
gl::DeleteTextures(1, std::ptr::addr_of_mut!(self.texture_id));
self.texture_id = 0;
gl::DeleteRenderbuffers(1, std::ptr::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, std::ptr::addr_of_mut!(self.fbo_id));
gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id);
gl::GenTextures(1, std::ptr::addr_of_mut!(self.texture_id));
gl::BindTexture(gl::TEXTURE_2D, self.texture_id);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGBA8 as i32,
width as i32,
height as i32,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
gl::GenRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id));
gl::BindRenderbuffer(gl::RENDERBUFFER, self.renderbuffer_id);
gl::RenderbufferStorage(
gl::RENDERBUFFER,
gl::DEPTH_COMPONENT,
width as i32,
height as i32,
);
gl::FramebufferTexture2D(
gl::FRAMEBUFFER,
gl::COLOR_ATTACHMENT0,
gl::TEXTURE_2D,
self.texture_id,
0,
);
gl::BindTexture(gl::TEXTURE_2D, 0);
gl::FramebufferRenderbuffer(
gl::FRAMEBUFFER,
gl::DEPTH_ATTACHMENT,
gl::RENDERBUFFER,
self.renderbuffer_id,
);
gl::BindRenderbuffer(gl::RENDERBUFFER, 0);
gl::Viewport(0, 0, width as i32, height as i32);
gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
// 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.
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 {
frontend.run_frame();
std::thread::sleep(step_duration);
}
}
}
impl FrontendInterface for App {
fn video_resize(&mut self, width: u32, height: u32) {
tracing::info!("Resized to {width}x{height}");
// Resize OpenGL resources if we need to.
if self.hw_render {
self.hw_gl_create_fbo(width, height);
}
self.rfb_server.resize(width as u16, height as u16);
}
fn video_update(&mut self, slice: &[u32], pitch: u32) {
//let framebuffer_size = self.get_frontend().get_size();
self.rfb_server.update_buffer(&slice, pitch, false);
}
fn video_update_gl(&mut self) {
let dimensions = self.get_frontend().get_size();
unsafe {
gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id);
// I know this sucks but it works for this case.
gl::ReadPixels(
0,
0,
dimensions.0 as i32,
dimensions.1 as i32,
gl::RGBA,
gl::UNSIGNED_BYTE,
self.readback_buffer.as_mut_ptr() as *mut std::ffi::c_void,
);
self.rfb_server
.update_buffer(&self.readback_buffer[..], dimensions.0, true);
gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
}
}
fn audio_sample(&mut self, _slice: &[i16], _size: usize) {}
fn input_poll(&mut self) {
self.pad.reset();
// Press all buttons the VNC server marked as pressed
let buttons = self.rfb_server.get_buttons();
for i in 0..buttons.len() {
if buttons[i] {
self.pad.press_button(i as u32, None);
}
}
}
fn hw_gl_init(&mut self) -> HwGlInitData {
if self.hw_render {
panic!("Cannot initalize HW rendering more than once");
}
unsafe {
// Initalize EGL
self.hw_gl_egl_init();
// load OpenGL functions (using EGL loader. We should probably check the one extension exists)
gl::load_with(|s| {
let str = std::ffi::CString::new(s).expect("Uhh huh.");
std::mem::transmute(egl::GetProcAddress(str.as_ptr()))
});
// set OpenGL debug message callback
gl::Enable(gl::DEBUG_OUTPUT);
gl::DebugMessageCallback(Some(opengl_message_callback), std::ptr::null());
// 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.hw_render = true;
return unsafe {
HwGlInitData {
get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void),
}
};
}
}

View file

@ -1,392 +1,18 @@
use std::{net::Ipv4Addr, path::Path, time::Duration};
use anyhow::Result; use anyhow::Result;
use std::net::Ipv4Addr;
use retro_frontend::{
frontend::{Frontend, FrontendInterface, HwGlInitData},
input_devices::{InputDevice, RetroPad},
};
use tracing::Level; use tracing::Level;
use tracing_subscriber::FmtSubscriber; use tracing_subscriber::FmtSubscriber;
use clap::{arg, command, value_parser}; use clap::{arg, command, value_parser};
/// Called by OpenGL. We use this to dump errors. mod app;
extern "system" fn opengl_message_callback(
source: gl::types::GLenum,
_type: gl::types::GLenum,
id: gl::types::GLuint,
_severity: gl::types::GLenum,
_length: gl::types::GLsizei,
message: *const gl::types::GLchar,
_user: *mut std::ffi::c_void,
) {
unsafe {
let message = std::ffi::CStr::from_ptr(message);
if _type == gl::DEBUG_TYPE_ERROR {
tracing::error!(
"OpenGL error: {:?} (res {:08x}, id = {:08x}, source = {:08x})",
message,
_type,
id,
source
);
}
}
}
mod egl; mod egl;
mod rfb; mod rfb;
use rfb::*;
struct App { use app::*;
frontend: Option<Box<Frontend>>, use rfb::RfbServerConfig;
rfb_server: Box<RfbServer>,
pad: RetroPad,
hw_render: 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,
/// Cached readback buffer.
readback_buffer: Vec<u32>,
}
impl App {
fn new(rfb_config: RfbServerConfig) -> Result<Box<Self>> {
let mut boxed = Box::new(Self {
frontend: None,
rfb_server: RfbServer::new(rfb_config)?,
pad: RetroPad::new(),
hw_render: false,
egl_display: std::ptr::null(),
egl_context: std::ptr::null(),
texture_id: 0,
renderbuffer_id: 0,
fbo_id: 0,
readback_buffer: Vec::new(),
});
// SAFETY: The boxed allocation will never drop since the main loop always loops forever.
// Even if it did, the only way to touch the pointer involves the frontend library calling retro_run,
// and the core calling one of the given callbacks. Therefore this is gnarly, but "fine".
//
// I'm still not really sure how to tell the borrow checker that this is alright,
// short of Box::leak() (which I don't want to do, since ideally I'd like actual cleanup to occur).
let obj = &mut *boxed as &mut dyn FrontendInterface;
boxed.frontend = Some(Frontend::new(obj as *mut dyn FrontendInterface));
Ok(boxed)
}
fn get_frontend(&mut self) -> &mut Frontend {
self.frontend.as_mut().unwrap()
}
fn init(&mut self) {
// Currently retrovnc just hardcodes the assumption of a single RetroPad.
// SAFETY: This too won't ever be Use-After-Free'd because the only oppoturnity to
// goes away on drop as well. That's a bit flaky reasoning wise, but is true.
//
// In all honesty, I'm not sure this even needs to be a *mut so I could see if
// making it a immutable reference works.
let pad = &mut self.pad as *mut dyn InputDevice;
self.get_frontend().plug_input_device(0, pad);
// Initalize the display
self.init_display();
}
fn init_display(&mut self) {
let av_info = self.get_frontend().get_av_info().expect("No AV info");
// Start VNC server.
{
let server = &mut self.rfb_server;
tracing::info!("Starting VNC server");
server.start();
server.resize(
av_info.geometry.base_width as u16,
av_info.geometry.base_height as u16,
);
}
}
fn load_core<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
// Unload an existing core.
if self.get_frontend().core_loaded() {
let _ = self.get_frontend().unload_core();
}
self.get_frontend().load_core(path)?;
Ok(())
}
fn load_game<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
self.get_frontend().load_game(path)?;
Ok(())
}
/// Initalizes a headless EGL context for OpenGL rendering.
unsafe fn hw_gl_egl_init(&mut self) {
self.egl_display = egl::get_device_platform_display();
self.egl_context = {
const EGL_CONFIG_ATTRIBUTES: [egl::types::EGLenum; 13] = [
egl::SURFACE_TYPE,
egl::PBUFFER_BIT,
egl::BLUE_SIZE,
8,
egl::RED_SIZE,
8,
egl::BLUE_SIZE,
8,
egl::DEPTH_SIZE,
8,
egl::RENDERABLE_TYPE,
egl::OPENGL_BIT,
egl::NONE,
];
let mut egl_major: egl::EGLint = 0;
let mut egl_minor: egl::EGLint = 0;
let mut egl_config_count: egl::EGLint = 0;
let mut config: egl::types::EGLConfig = std::ptr::null();
egl::Initialize(
self.egl_display,
std::ptr::addr_of_mut!(egl_major),
std::ptr::addr_of_mut!(egl_minor),
);
egl::ChooseConfig(
self.egl_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(self.egl_display, config, egl::NO_CONTEXT, std::ptr::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.
unsafe fn hw_gl_egl_exit(&mut self) {
// Release the EGL context we created before destroying it
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 = std::ptr::null();
self.egl_context = std::ptr::null();
}
/// 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, std::ptr::addr_of_mut!(self.fbo_id));
self.fbo_id = 0;
gl::DeleteTextures(1, std::ptr::addr_of_mut!(self.texture_id));
self.texture_id = 0;
gl::DeleteRenderbuffers(1, std::ptr::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, std::ptr::addr_of_mut!(self.fbo_id));
gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id);
gl::GenTextures(1, std::ptr::addr_of_mut!(self.texture_id));
gl::BindTexture(gl::TEXTURE_2D, self.texture_id);
gl::TexImage2D(
gl::TEXTURE_2D,
0,
gl::RGBA8 as i32,
width as i32,
height as i32,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
std::ptr::null(),
);
gl::GenRenderbuffers(1, std::ptr::addr_of_mut!(self.renderbuffer_id));
gl::BindRenderbuffer(gl::RENDERBUFFER, self.renderbuffer_id);
gl::RenderbufferStorage(
gl::RENDERBUFFER,
gl::DEPTH_COMPONENT,
width as i32,
height as i32,
);
gl::FramebufferTexture2D(
gl::FRAMEBUFFER,
gl::COLOR_ATTACHMENT0,
gl::TEXTURE_2D,
self.texture_id,
0,
);
gl::BindTexture(gl::TEXTURE_2D, 0);
gl::FramebufferRenderbuffer(
gl::FRAMEBUFFER,
gl::DEPTH_ATTACHMENT,
gl::RENDERBUFFER,
self.renderbuffer_id,
);
gl::BindRenderbuffer(gl::RENDERBUFFER, 0);
gl::Viewport(0, 0, width as i32, height as i32);
gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
// 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);
}
}
// Main loop
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 {
frontend.run_frame();
std::thread::sleep(step_duration);
}
}
}
impl FrontendInterface for App {
fn video_resize(&mut self, width: u32, height: u32) {
tracing::info!("Resized to {width}x{height}");
// Resize OpenGL resources if we need to.
if self.hw_render {
self.hw_gl_create_fbo(width, height);
}
self.rfb_server.resize(width as u16, height as u16);
}
fn video_update(&mut self, slice: &[u32], pitch: u32) {
//let framebuffer_size = self.get_frontend().get_size();
self.rfb_server.update_buffer(&slice, pitch, false);
}
fn video_update_gl(&mut self) {
let dimensions = self.get_frontend().get_size();
unsafe {
gl::BindFramebuffer(gl::FRAMEBUFFER, self.fbo_id);
// I know this sucks but it works for this case.
gl::ReadPixels(
0,
0,
dimensions.0 as i32,
dimensions.1 as i32,
gl::RGBA,
gl::UNSIGNED_BYTE,
self.readback_buffer.as_mut_ptr() as *mut std::ffi::c_void,
);
self.rfb_server
.update_buffer(&self.readback_buffer[..], dimensions.0, true);
gl::BindFramebuffer(gl::FRAMEBUFFER, 0);
}
}
fn audio_sample(&mut self, _slice: &[i16], _size: usize) {}
fn input_poll(&mut self) {
self.pad.reset();
// Press all buttons the VNC server marked as pressed
let buttons = self.rfb_server.get_buttons();
for i in 0..buttons.len() {
if buttons[i] {
self.pad.press_button(i as u32, None);
}
}
}
fn hw_gl_init(&mut self) -> HwGlInitData {
if self.hw_render {
panic!("Cannot initalize HW rendering more than once");
}
unsafe {
// Initalize EGL
self.hw_gl_egl_init();
// load OpenGL functions (using EGL loader. We should probably check the one extension exists)
gl::load_with(|s| {
let str = std::ffi::CString::new(s).expect("Uhh huh.");
std::mem::transmute(egl::GetProcAddress(str.as_ptr()))
});
// set OpenGL debug message callback
gl::Enable(gl::DEBUG_OUTPUT);
gl::DebugMessageCallback(Some(opengl_message_callback), std::ptr::null());
// 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.hw_render = true;
return unsafe {
HwGlInitData {
get_proc_address: std::mem::transmute(egl::GetProcAddress as *mut std::ffi::c_void),
}
};
}
}
fn main() -> Result<()> { fn main() -> Result<()> {
// Setup a tracing subscriber // Setup a tracing subscriber