works
This commit is contained in:
parent
9b696d3f8b
commit
1c86b877ca
4 changed files with 735 additions and 18 deletions
99
server/Cargo.lock
generated
99
server/Cargo.lock
generated
|
@ -714,6 +714,16 @@ dependencies = [
|
|||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.46.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
|
||||
dependencies = [
|
||||
"overload",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_cpus"
|
||||
version = "1.16.0"
|
||||
|
@ -739,6 +749,12 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.3"
|
||||
|
@ -1026,6 +1042,15 @@ dependencies = [
|
|||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
|
@ -1109,6 +1134,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinyvec"
|
||||
version = "1.8.0"
|
||||
|
@ -1257,6 +1292,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b"
|
||||
dependencies = [
|
||||
"nu-ansi-term",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1322,6 +1383,12 @@ version = "0.7.6"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
|
@ -1344,12 +1411,16 @@ dependencies = [
|
|||
"ffmpeg-next",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"gl",
|
||||
"letsplay_gpu",
|
||||
"rand",
|
||||
"retro_frontend",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1358,6 +1429,28 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
|
@ -1440,6 +1533,12 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.22"
|
||||
|
|
|
@ -7,8 +7,10 @@ edition = "2021"
|
|||
|
||||
anyhow = "1.0.86"
|
||||
|
||||
# Libretro Sex
|
||||
letsplay_gpu.path = "/home/lily/source/lets-play/crates/letsplay_gpu"
|
||||
retro_frontend.path = "/home/lily/source/lets-play/crates/retro_frontend"
|
||||
gl = "0.14.0"
|
||||
|
||||
# async
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
|
@ -26,6 +28,9 @@ rand = "0.8.5"
|
|||
serde = "1.0.209"
|
||||
serde_json = "1.0.128"
|
||||
cudarc = { version = "0.12.1", features = [ "cuda-11050" ] }
|
||||
tracing = "0.1.40"
|
||||
tracing-subscriber = "0.3.18"
|
||||
xkeysym = "0.2.1"
|
||||
|
||||
|
||||
[patch.crates-io]
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
mod retro_thread;
|
||||
mod surface;
|
||||
mod types;
|
||||
mod video;
|
||||
|
||||
use retro_thread::{spawn_retro_thread, App, RetroEvent};
|
||||
use video::ffmpeg;
|
||||
use video::h264_encoder::H264Encoder;
|
||||
|
||||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use rand::distributions::DistString;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::sync::{
|
||||
broadcast,
|
||||
mpsc::{self, error::TryRecvError},
|
||||
Mutex as TokioMutex,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
|
@ -32,18 +36,18 @@ use futures::{sink::SinkExt, stream::StreamExt};
|
|||
|
||||
struct AppState {
|
||||
encoder_tx: Arc<TokioMutex<mpsc::Sender<()>>>,
|
||||
inputs: Arc<TokioMutex<Vec<u32>>>,
|
||||
|
||||
websocket_broadcast_tx: broadcast::Sender<ws::Message>,
|
||||
websocket_count: TokioMutex<usize>,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
fn new(
|
||||
encoder_tx: mpsc::Sender<()>,
|
||||
) -> Self {
|
||||
fn new(encoder_tx: mpsc::Sender<()>) -> Self {
|
||||
let (chat_tx, _chat_rx) = broadcast::channel(10);
|
||||
Self {
|
||||
encoder_tx: Arc::new(TokioMutex::new(encoder_tx)),
|
||||
inputs: Arc::new(TokioMutex::new(Vec::new())),
|
||||
websocket_broadcast_tx: chat_tx,
|
||||
websocket_count: TokioMutex::const_new(0usize),
|
||||
}
|
||||
|
@ -74,7 +78,7 @@ impl EncoderState {
|
|||
|
||||
// FIXME: use create_frame() on H264Encoder
|
||||
self.frame = Some(ffmpeg::frame::Video::new(
|
||||
ffmpeg::format::Pixel::BGRA,
|
||||
ffmpeg::format::Pixel::RGBA,
|
||||
size.width,
|
||||
size.height,
|
||||
));
|
||||
|
@ -109,7 +113,9 @@ impl EncoderState {
|
|||
}
|
||||
|
||||
encoder.send_frame(&*frame);
|
||||
encoder.receive_packet(&mut self.packet).expect("Failed to recieve packet");
|
||||
encoder
|
||||
.receive_packet(&mut self.packet)
|
||||
.expect("Failed to recieve packet");
|
||||
|
||||
unsafe {
|
||||
if !self.packet.is_empty() {
|
||||
|
@ -123,6 +129,13 @@ impl EncoderState {
|
|||
|
||||
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
// Setup a tracing subscriber
|
||||
let subscriber = tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||
|
||||
let surface = Arc::new(Mutex::new(surface::Surface::new()));
|
||||
|
||||
// H.264 encoder related
|
||||
|
@ -131,6 +144,126 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let state = Arc::new(AppState::new(encoder_tx));
|
||||
|
||||
let (mut event_rx, event_in_tx) = spawn_retro_thread(surface.clone());
|
||||
|
||||
let state_clone = state.clone();
|
||||
let encoder_state_clone = encoder_state.clone();
|
||||
|
||||
let vnc_recv_handle = tokio::spawn(async move {
|
||||
let surface_clone = surface.clone();
|
||||
|
||||
// first frame is always a key frame
|
||||
let mut pts = 0u64;
|
||||
let mut force_keyframe = true;
|
||||
let mut frame_update = false;
|
||||
|
||||
// start the thread now that we're alive
|
||||
let _ = event_in_tx.send(retro_thread::RetroInEvent::Start).await;
|
||||
|
||||
loop {
|
||||
match encoder_rx.try_recv() {
|
||||
Ok(()) => {
|
||||
// force keyframe
|
||||
force_keyframe = true;
|
||||
frame_update = true;
|
||||
}
|
||||
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
Err(TryRecvError::Empty) => {}
|
||||
}
|
||||
|
||||
match event_rx.try_recv() {
|
||||
Ok(msg) => match msg {
|
||||
RetroEvent::Frame => {
|
||||
{
|
||||
let mut state_locked = encoder_state_clone.lock().await;
|
||||
|
||||
let mut_frame = state_locked.frame();
|
||||
|
||||
let width = mut_frame.width();
|
||||
let height = mut_frame.height();
|
||||
|
||||
let mut surf = surface_clone.lock().expect(
|
||||
"locking the VNC surface to paint it to the ffmpeg frame failed",
|
||||
);
|
||||
let surf_buf = surf.get_buffer();
|
||||
|
||||
let buf_ptr =
|
||||
unsafe { (*(*mut_frame.as_mut_ptr()).buf[0]).data as *mut u32 };
|
||||
|
||||
for y in 0..height {
|
||||
let line_stride = (y * width) as usize;
|
||||
// Make a slice for the line
|
||||
// SAFETY: The allocation is guaranteed to be large enough
|
||||
// for this to work from y = 0..height
|
||||
let dest_line_slice = unsafe {
|
||||
let dest_line_ptr = buf_ptr.add(line_stride);
|
||||
std::slice::from_raw_parts_mut(dest_line_ptr, width as usize)
|
||||
};
|
||||
|
||||
dest_line_slice.copy_from_slice(
|
||||
&surf_buf[line_stride..line_stride + width as usize],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
frame_update = true;
|
||||
}
|
||||
|
||||
RetroEvent::Resize { size } => {
|
||||
{
|
||||
let mut state_locked = encoder_state_clone.lock().await;
|
||||
state_locked.init(size).expect("fuck you");
|
||||
|
||||
// reset our internal state
|
||||
pts = 0;
|
||||
force_keyframe = true;
|
||||
frame_update = false;
|
||||
}
|
||||
}
|
||||
|
||||
RetroEvent::WantInputs { tx } => {
|
||||
let inputs = state_clone.inputs.lock().await;
|
||||
//tracing::info!("giving inputs {:?}", inputs);
|
||||
tx.send(inputs.clone()).expect("FUCK");
|
||||
}
|
||||
},
|
||||
|
||||
Err(TryRecvError::Disconnected) => break,
|
||||
Err(TryRecvError::Empty) => {}
|
||||
}
|
||||
|
||||
// send frame if we should.
|
||||
if frame_update {
|
||||
let mut state_locked = encoder_state_clone.lock().await;
|
||||
|
||||
match state_locked.send_frame(pts, force_keyframe) {
|
||||
Some(mut packet) => {
|
||||
let vec = {
|
||||
let data = packet.data_mut().expect("packet is empty somehow");
|
||||
data.to_vec()
|
||||
};
|
||||
|
||||
let _ = state_clone
|
||||
.websocket_broadcast_tx
|
||||
.send(ws::Message::Binary(vec));
|
||||
|
||||
pts += 1;
|
||||
|
||||
if force_keyframe {
|
||||
force_keyframe = false;
|
||||
}
|
||||
}
|
||||
|
||||
None => {}
|
||||
}
|
||||
|
||||
frame_update = false;
|
||||
}
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(1)).await;
|
||||
}
|
||||
});
|
||||
|
||||
// Axum websocket server
|
||||
let app: Router<()> = Router::new()
|
||||
|
@ -260,6 +393,21 @@ async fn handle_socket(socket: WebSocket, who: SocketAddr, state: Arc<AppState>)
|
|||
let keysym = json["keysym"].as_u64().unwrap() as u32;
|
||||
let pressed = json["pressed"].as_u64().unwrap() == 1;
|
||||
|
||||
// FIXME: This would be MUCH better off being a set, so we don't
|
||||
// hack-code set semantics here. Oh well.
|
||||
{
|
||||
let mut lock = recv_clone.inputs.lock().await;
|
||||
if pressed {
|
||||
if let None = lock.iter().position(|e| *e == keysym) {
|
||||
lock.push(keysym);
|
||||
}
|
||||
} else {
|
||||
if let Some(at) = lock.iter().position(|e| *e == keysym) {
|
||||
lock.remove(at);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*let _ = recv_clone
|
||||
.engine_tx
|
||||
.send(vnc_engine::VncMessageInput::KeyEvent {
|
||||
|
|
465
server/src/retro_thread.rs
Normal file
465
server/src/retro_thread.rs
Normal file
|
@ -0,0 +1,465 @@
|
|||
use std::{
|
||||
path::Path,
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use retro_frontend::{
|
||||
frontend::{Frontend, FrontendInterface, HwGlInitData},
|
||||
input_devices::{InputDevice, RetroPad},
|
||||
libretro_sys_new,
|
||||
};
|
||||
|
||||
use gpu::egl_helpers::DeviceContext;
|
||||
use letsplay_gpu as gpu;
|
||||
|
||||
use crate::{
|
||||
surface::Surface,
|
||||
types::{Rect, Size},
|
||||
};
|
||||
|
||||
/// 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 {
|
||||
frontend: Option<Box<Frontend>>,
|
||||
|
||||
pad: RetroPad,
|
||||
|
||||
// EGL state
|
||||
egl_context: Option<DeviceContext>,
|
||||
|
||||
framebuffer: Arc<Mutex<Surface>>,
|
||||
|
||||
// OpenGL object IDs
|
||||
gl_framebuffer: gpu::GlFramebuffer,
|
||||
|
||||
/// Cached readback buffer.
|
||||
readback_buffer: Surface,
|
||||
|
||||
event_tx: mpsc::Sender<RetroEvent>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(framebuffer: Arc<Mutex<Surface>>, event_tx: mpsc::Sender<RetroEvent>) -> Box<Self> {
|
||||
let mut boxed = Box::new(Self {
|
||||
frontend: None,
|
||||
pad: RetroPad::new(),
|
||||
|
||||
egl_context: None,
|
||||
framebuffer,
|
||||
gl_framebuffer: gpu::GlFramebuffer::new(),
|
||||
readback_buffer: Surface::new(),
|
||||
event_tx,
|
||||
});
|
||||
|
||||
// SAFETY: 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",
|
||||
// since once the main loop ends, there won't be an opporturnity for said callbacks to be called again.
|
||||
//
|
||||
// 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));
|
||||
|
||||
boxed
|
||||
}
|
||||
|
||||
fn get_frontend(&mut self) -> &mut Frontend {
|
||||
self.frontend.as_mut().unwrap()
|
||||
}
|
||||
|
||||
/// Inserts our RetroPad and initalizes the display.
|
||||
pub fn init(&mut self) {
|
||||
// SAFETY: This too won't ever be Use-After-Free'd because the only chance 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);
|
||||
|
||||
self.init_display();
|
||||
}
|
||||
|
||||
fn init_display(&mut self) {
|
||||
let av_info = self.get_frontend().get_av_info().expect("No AV info");
|
||||
|
||||
//self.window.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 the headless EGL context used for OpenGL rendering.
|
||||
fn hw_gl_egl_init(&mut self) {
|
||||
self.egl_context = Some(DeviceContext::new(0));
|
||||
}
|
||||
|
||||
/// Destroys OpenGL resources and the EGL context.
|
||||
fn hw_gl_destroy(&mut self) {
|
||||
if self.egl_context.is_some() {
|
||||
self.gl_framebuffer.destroy();
|
||||
self.egl_context.take().unwrap().destroy()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
let av_info = self.get_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);
|
||||
|
||||
self.get_frontend().run_frame();
|
||||
let _ = self.event_tx.blocking_send(RetroEvent::Frame);
|
||||
std::thread::sleep(step_duration);
|
||||
}
|
||||
|
||||
/// Bleh, I don't like this is an associated fn, but whatever
|
||||
fn update_impl(framebuffer: Arc<Mutex<Surface>>, slice: &[u32], pitch: u32, from_opengl: bool) {
|
||||
let mut framebuffer_locked = framebuffer.lock().expect("could not lock framebuffer");
|
||||
let size = framebuffer_locked.size.clone();
|
||||
|
||||
let has_disconnected_pitch = pitch != size.width as u32;
|
||||
|
||||
// If this frame came from OpenGL we need to flip the image around
|
||||
// so it is right side up (from our perspective).
|
||||
if from_opengl {
|
||||
let mut scanlines: Vec<&[u32]> = Vec::with_capacity(size.height as usize);
|
||||
|
||||
// Push scanline slices in reverse order (which will actually flip them to the right orientation)
|
||||
for y in (0..size.height).rev() {
|
||||
let src_line_off = (y as u32 * pitch) as usize;
|
||||
let src_slice = &slice[src_line_off..src_line_off + size.width as usize];
|
||||
scanlines.push(src_slice);
|
||||
}
|
||||
|
||||
// Draw them
|
||||
for y in 0..size.height {
|
||||
let src_line_off = (y as u32 * pitch) as usize;
|
||||
let mut dest_line_off = src_line_off;
|
||||
|
||||
// copy only
|
||||
if has_disconnected_pitch {
|
||||
dest_line_off = (y * size.width.min(pitch)) as usize;
|
||||
}
|
||||
|
||||
let dest_slice = &mut framebuffer_locked.get_buffer()
|
||||
[dest_line_off..dest_line_off + size.width as usize];
|
||||
|
||||
dest_slice.copy_from_slice(scanlines[y as usize]);
|
||||
|
||||
// swap the scanline pixels to BGRA order to make minifb happy
|
||||
// not the fastest code but this should do for an example
|
||||
//for pix in dest_slice {
|
||||
// let a = (*pix & 0xff000000) >> 24;
|
||||
// let b = (*pix & 0x00ff0000) >> 16;
|
||||
// let g = (*pix & 0x0000ff00) >> 8;
|
||||
// let r = *pix & 0x000000ff;
|
||||
// *pix = a << 24 | r << 16 | g << 8 | b;
|
||||
//}
|
||||
}
|
||||
} else {
|
||||
for y in 0..size.height {
|
||||
let src_line_off = (y as u32 * pitch) as usize;
|
||||
let mut dest_line_off = src_line_off;
|
||||
|
||||
// copy only
|
||||
if has_disconnected_pitch {
|
||||
dest_line_off = (y * size.width) as usize;
|
||||
}
|
||||
|
||||
// Create slices repressenting each part
|
||||
let src_slice = &slice[src_line_off..src_line_off + size.width as usize];
|
||||
let dest_slice = &mut framebuffer_locked.get_buffer()
|
||||
[dest_line_off..dest_line_off + size.width as usize];
|
||||
|
||||
dest_slice.copy_from_slice(src_slice);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FrontendInterface for App {
|
||||
fn video_resize(&mut self, width: u32, height: u32) {
|
||||
tracing::info!("Resized to {width}x{height}");
|
||||
|
||||
if self.egl_context.is_some() {
|
||||
self.gl_framebuffer.resize(width, height);
|
||||
let raw = self.gl_framebuffer.as_raw();
|
||||
|
||||
// Notify the frontend layer about the new FBO ID
|
||||
self.get_frontend().set_gl_fbo(raw);
|
||||
|
||||
// Resize the readback buffer
|
||||
self.readback_buffer.resize(Size { width, height });
|
||||
}
|
||||
|
||||
self.framebuffer
|
||||
.lock()
|
||||
.expect("its over?")
|
||||
.resize(Size { width, height });
|
||||
|
||||
let _ = self.event_tx.blocking_send(RetroEvent::Resize {
|
||||
size: Size { width, height },
|
||||
});
|
||||
}
|
||||
|
||||
fn video_update(&mut self, slice: &[u32], pitch: u32) {
|
||||
Self::update_impl(self.framebuffer.clone(), slice, pitch, false);
|
||||
}
|
||||
|
||||
fn video_update_gl(&mut self) {
|
||||
let dimensions = self.get_frontend().get_size();
|
||||
|
||||
// Read back the framebuffer
|
||||
let slice = {
|
||||
//self.gl_framebuffer.read_pixels(
|
||||
// &mut self.readback_buffer.get_buffer()[..],
|
||||
// dimensions.0,
|
||||
// dimensions.1,
|
||||
//);
|
||||
|
||||
unsafe {
|
||||
let scope = self.gl_framebuffer.bind();
|
||||
gl::ReadPixels(
|
||||
0,
|
||||
0,
|
||||
dimensions.0 as i32,
|
||||
dimensions.1 as i32,
|
||||
gl::BGRA,
|
||||
gl::UNSIGNED_BYTE,
|
||||
(&mut self.readback_buffer.get_buffer()).as_mut_ptr() as *mut std::ffi::c_void,
|
||||
);
|
||||
}
|
||||
|
||||
self.readback_buffer.get_buffer()
|
||||
};
|
||||
|
||||
Self::update_impl(self.framebuffer.clone(), slice, dimensions.0, true);
|
||||
}
|
||||
|
||||
fn audio_sample(&mut self, _slice: &[i16], _size: usize) {}
|
||||
|
||||
fn input_poll(&mut self) {
|
||||
self.pad.reset();
|
||||
|
||||
let (tx, rx) = oneshot::channel();
|
||||
let _ = self.event_tx.blocking_send(RetroEvent::WantInputs { tx });
|
||||
|
||||
let inputs = rx.blocking_recv().expect("what the FUCK are you doing");
|
||||
|
||||
for key in &inputs {
|
||||
use xkeysym::key as Key;
|
||||
|
||||
match *key {
|
||||
Key::backslash => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_SELECT, None);
|
||||
}
|
||||
Key::Return => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_START, None);
|
||||
}
|
||||
|
||||
Key::Up => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_UP, None);
|
||||
}
|
||||
Key::Down => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_DOWN, None);
|
||||
}
|
||||
Key::Left => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_LEFT, None);
|
||||
}
|
||||
Key::Right => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_RIGHT, None);
|
||||
}
|
||||
|
||||
Key::s => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_B, None);
|
||||
}
|
||||
|
||||
Key::a => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_A, None);
|
||||
}
|
||||
|
||||
Key::q => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_X, None);
|
||||
}
|
||||
|
||||
Key::w => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_Y, None);
|
||||
}
|
||||
|
||||
Key::Control_L => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_L, None);
|
||||
}
|
||||
|
||||
Key::Shift_L => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_L2, None);
|
||||
}
|
||||
|
||||
Key::Alt_L => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_R, None);
|
||||
}
|
||||
|
||||
Key::z => {
|
||||
self.pad
|
||||
.press_button(libretro_sys_new::DEVICE_ID_JOYPAD_R2, None);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn hw_gl_init(&mut self) -> Option<HwGlInitData> {
|
||||
// Only create a new EGL/OpenGL context if we have to.
|
||||
if self.egl_context.is_none() {
|
||||
// Initalize EGL
|
||||
self.hw_gl_egl_init();
|
||||
|
||||
let context = self.egl_context.as_ref().unwrap();
|
||||
let extensions = gpu::egl_helpers::get_extensions(context.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
|
||||
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.");
|
||||
tracing::error!("Retrodemo currently needs this to load OpenGL functions. HW rendering will be disabled.");
|
||||
return None;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Load OpenGL functions using the EGL loader.
|
||||
gl::load_with(|s| {
|
||||
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), std::ptr::null());
|
||||
}
|
||||
}
|
||||
|
||||
// Create the initial FBO for the core to render to
|
||||
let dimensions = self.get_frontend().get_size();
|
||||
self.gl_framebuffer.resize(dimensions.0, dimensions.1);
|
||||
|
||||
return Some(HwGlInitData {
|
||||
get_proc_address: gpu::egl::GetProcAddress as *mut std::ffi::c_void,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for App {
|
||||
fn drop(&mut self) {
|
||||
// Terminate EGL and GL resources if need be
|
||||
self.hw_gl_destroy();
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RetroEvent {
|
||||
Frame,
|
||||
Resize { size: Size },
|
||||
WantInputs { tx: oneshot::Sender<Vec<u32>> },
|
||||
}
|
||||
|
||||
pub enum RetroInEvent {
|
||||
Start,
|
||||
}
|
||||
|
||||
fn retro_thread_main(
|
||||
surface: Arc<Mutex<Surface>>,
|
||||
event_tx: mpsc::Sender<RetroEvent>,
|
||||
mut event_rx: mpsc::Receiver<RetroInEvent>,
|
||||
) {
|
||||
let mut app = App::new(surface, event_tx);
|
||||
|
||||
app.load_core("cores/swanstation_libretro.so")
|
||||
.expect("failed to load core");
|
||||
app.load_game("roms/merged/nmv1/us/nmv1_us.cue") //merged/nmv1/us/nmv1_us.cue
|
||||
.expect("failed to load game");
|
||||
|
||||
// sync
|
||||
loop {
|
||||
match event_rx.blocking_recv() {
|
||||
None => return (),
|
||||
Some(msg) => match msg {
|
||||
RetroInEvent::Start => break,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
app.init();
|
||||
|
||||
loop {
|
||||
app.tick();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_retro_thread(
|
||||
surface: Arc<Mutex<Surface>>,
|
||||
) -> (mpsc::Receiver<RetroEvent>, mpsc::Sender<RetroInEvent>) {
|
||||
let (event_tx, event_rx) = mpsc::channel(8);
|
||||
let (event_in_tx, event_in_rx) = mpsc::channel(8);
|
||||
let fb_clone = surface.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
retro_thread_main(fb_clone, event_tx, event_in_rx);
|
||||
});
|
||||
|
||||
(event_rx, event_in_tx)
|
||||
}
|
Loading…
Reference in a new issue