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",
|
"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]]
|
[[package]]
|
||||||
name = "num_cpus"
|
name = "num_cpus"
|
||||||
version = "1.16.0"
|
version = "1.16.0"
|
||||||
|
@ -739,6 +749,12 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "overload"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
@ -1026,6 +1042,15 @@ dependencies = [
|
||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
|
@ -1109,6 +1134,16 @@ dependencies = [
|
||||||
"syn",
|
"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]]
|
[[package]]
|
||||||
name = "tinyvec"
|
name = "tinyvec"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -1257,6 +1292,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
|
@ -1322,6 +1383,12 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "valuable"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -1344,12 +1411,16 @@ dependencies = [
|
||||||
"ffmpeg-next",
|
"ffmpeg-next",
|
||||||
"futures",
|
"futures",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"gl",
|
||||||
"letsplay_gpu",
|
"letsplay_gpu",
|
||||||
"rand",
|
"rand",
|
||||||
"retro_frontend",
|
"retro_frontend",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"xkeysym",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1358,6 +1429,28 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
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]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
@ -1440,6 +1533,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkeysym"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "xml-rs"
|
name = "xml-rs"
|
||||||
version = "0.8.22"
|
version = "0.8.22"
|
||||||
|
|
|
@ -7,8 +7,10 @@ edition = "2021"
|
||||||
|
|
||||||
anyhow = "1.0.86"
|
anyhow = "1.0.86"
|
||||||
|
|
||||||
|
# Libretro Sex
|
||||||
letsplay_gpu.path = "/home/lily/source/lets-play/crates/letsplay_gpu"
|
letsplay_gpu.path = "/home/lily/source/lets-play/crates/letsplay_gpu"
|
||||||
retro_frontend.path = "/home/lily/source/lets-play/crates/retro_frontend"
|
retro_frontend.path = "/home/lily/source/lets-play/crates/retro_frontend"
|
||||||
|
gl = "0.14.0"
|
||||||
|
|
||||||
# async
|
# async
|
||||||
tokio = { version = "1.39.3", features = ["full"] }
|
tokio = { version = "1.39.3", features = ["full"] }
|
||||||
|
@ -26,6 +28,9 @@ rand = "0.8.5"
|
||||||
serde = "1.0.209"
|
serde = "1.0.209"
|
||||||
serde_json = "1.0.128"
|
serde_json = "1.0.128"
|
||||||
cudarc = { version = "0.12.1", features = [ "cuda-11050" ] }
|
cudarc = { version = "0.12.1", features = [ "cuda-11050" ] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
xkeysym = "0.2.1"
|
||||||
|
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
|
mod retro_thread;
|
||||||
mod surface;
|
mod surface;
|
||||||
mod types;
|
mod types;
|
||||||
mod video;
|
mod video;
|
||||||
|
|
||||||
|
use retro_thread::{spawn_retro_thread, App, RetroEvent};
|
||||||
use video::ffmpeg;
|
use video::ffmpeg;
|
||||||
use video::h264_encoder::H264Encoder;
|
use video::h264_encoder::H264Encoder;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand::distributions::DistString;
|
use rand::distributions::DistString;
|
||||||
|
use std::net::SocketAddr;
|
||||||
use tokio::sync::{
|
use tokio::sync::{
|
||||||
broadcast,
|
broadcast,
|
||||||
mpsc::{self, error::TryRecvError},
|
mpsc::{self, error::TryRecvError},
|
||||||
Mutex as TokioMutex,
|
Mutex as TokioMutex,
|
||||||
};
|
};
|
||||||
use std::net::SocketAddr;
|
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{
|
extract::{
|
||||||
|
@ -32,18 +36,18 @@ use futures::{sink::SinkExt, stream::StreamExt};
|
||||||
|
|
||||||
struct AppState {
|
struct AppState {
|
||||||
encoder_tx: Arc<TokioMutex<mpsc::Sender<()>>>,
|
encoder_tx: Arc<TokioMutex<mpsc::Sender<()>>>,
|
||||||
|
inputs: Arc<TokioMutex<Vec<u32>>>,
|
||||||
|
|
||||||
websocket_broadcast_tx: broadcast::Sender<ws::Message>,
|
websocket_broadcast_tx: broadcast::Sender<ws::Message>,
|
||||||
websocket_count: TokioMutex<usize>,
|
websocket_count: TokioMutex<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppState {
|
impl AppState {
|
||||||
fn new(
|
fn new(encoder_tx: mpsc::Sender<()>) -> Self {
|
||||||
encoder_tx: mpsc::Sender<()>,
|
|
||||||
) -> Self {
|
|
||||||
let (chat_tx, _chat_rx) = broadcast::channel(10);
|
let (chat_tx, _chat_rx) = broadcast::channel(10);
|
||||||
Self {
|
Self {
|
||||||
encoder_tx: Arc::new(TokioMutex::new(encoder_tx)),
|
encoder_tx: Arc::new(TokioMutex::new(encoder_tx)),
|
||||||
|
inputs: Arc::new(TokioMutex::new(Vec::new())),
|
||||||
websocket_broadcast_tx: chat_tx,
|
websocket_broadcast_tx: chat_tx,
|
||||||
websocket_count: TokioMutex::const_new(0usize),
|
websocket_count: TokioMutex::const_new(0usize),
|
||||||
}
|
}
|
||||||
|
@ -74,7 +78,7 @@ impl EncoderState {
|
||||||
|
|
||||||
// FIXME: use create_frame() on H264Encoder
|
// FIXME: use create_frame() on H264Encoder
|
||||||
self.frame = Some(ffmpeg::frame::Video::new(
|
self.frame = Some(ffmpeg::frame::Video::new(
|
||||||
ffmpeg::format::Pixel::BGRA,
|
ffmpeg::format::Pixel::RGBA,
|
||||||
size.width,
|
size.width,
|
||||||
size.height,
|
size.height,
|
||||||
));
|
));
|
||||||
|
@ -109,7 +113,9 @@ impl EncoderState {
|
||||||
}
|
}
|
||||||
|
|
||||||
encoder.send_frame(&*frame);
|
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 {
|
unsafe {
|
||||||
if !self.packet.is_empty() {
|
if !self.packet.is_empty() {
|
||||||
|
@ -123,6 +129,13 @@ impl EncoderState {
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
#[tokio::main(flavor = "multi_thread", worker_threads = 8)]
|
||||||
async fn main() -> anyhow::Result<()> {
|
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()));
|
let surface = Arc::new(Mutex::new(surface::Surface::new()));
|
||||||
|
|
||||||
// H.264 encoder related
|
// H.264 encoder related
|
||||||
|
@ -131,6 +144,126 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let state = Arc::new(AppState::new(encoder_tx));
|
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
|
// Axum websocket server
|
||||||
let app: Router<()> = Router::new()
|
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 keysym = json["keysym"].as_u64().unwrap() as u32;
|
||||||
let pressed = json["pressed"].as_u64().unwrap() == 1;
|
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
|
/*let _ = recv_clone
|
||||||
.engine_tx
|
.engine_tx
|
||||||
.send(vnc_engine::VncMessageInput::KeyEvent {
|
.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