mac frontend can actually run games now

This commit is contained in:
Alex Janka 2023-11-24 17:00:06 +11:00
parent 0610b8166d
commit e456e24f06
11 changed files with 429 additions and 95 deletions

13
Cargo.lock generated
View file

@ -1440,6 +1440,7 @@ dependencies = [
"send_wrapper",
"serde",
"twinc_emu_vst",
"uuid 1.6.1",
"winit",
"winit_input_helper",
]
@ -1511,7 +1512,7 @@ dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid 1.5.0",
"uuid 1.6.1",
"vec_map",
]
@ -1528,7 +1529,7 @@ dependencies = [
"libudev-sys",
"log",
"nix 0.26.4",
"uuid 1.5.0",
"uuid 1.6.1",
"vec_map",
"wasm-bindgen",
"web-sys",
@ -3846,9 +3847,13 @@ dependencies = [
[[package]]
name = "uuid"
version = "1.5.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc"
checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560"
dependencies = [
"getrandom",
"rand",
]
[[package]]
name = "vcpkg"

View file

@ -42,6 +42,7 @@ image = { version = "0.24", default-features = false, features = ["png"] }
bytemuck = "1.14"
chrono = "0.4"
twinc_emu_vst = { path = "../gb-vst", default-features = false }
uuid = { version = "1.6", features = ["v4", "fast-rng"] }
[target.'cfg(any(target_os = "macos"))'.dependencies]
cacao = { git = "https://github.com/italicsjenga/cacao" }

View file

@ -65,9 +65,8 @@ pub fn create_output(muted: bool) -> (AudioOutput, Stream) {
&config.config(),
move |data: &mut [f32], _info: &cpal::OutputCallbackInfo| {
for v in data.chunks_exact_mut(2) {
match executor::block_on(rx.pop()) {
Some(a) => v.copy_from_slice(&a),
None => panic!("Audio queue disconnected!"),
if let Some(a) = executor::block_on(rx.pop()) {
v.copy_from_slice(&a);
}
}
},

View file

@ -3,12 +3,17 @@
#[cfg(feature = "camera")]
use camera::Webcam;
use clap::{ArgGroup, Parser, Subcommand, ValueEnum};
use gb_emu::{audio, window::winit_manager::WinitWindowManager};
use gb_emu::{audio, debug::Debugger, window::winit_manager::WinitWindowManager};
use gb_emu_lib::{
config::ConfigManager,
connect::{EmulatorMessage, SerialTarget, SramType, StdoutType},
connect::{EmulatorCoreTrait, EmulatorMessage, SerialTarget, SramType, StdoutType},
};
use std::{
path::PathBuf,
process::exit,
sync::mpsc::channel,
time::{Duration, Instant},
};
use std::{path::PathBuf, sync::mpsc::channel};
#[cfg(all(feature = "vulkan", feature = "pixels"))]
compile_error!("select only one rendering backend!");
@ -160,7 +165,6 @@ fn main() {
}
}
} else {
gb_emu::init_configs();
let (sender, receiver) = channel::<EmulatorMessage>();
{
@ -170,11 +174,30 @@ fn main() {
})
.unwrap();
}
let record = args.record;
let mute = args.mute;
let (record, mute, debug) = (args.record, args.mute, args.debug);
let prepared = gb_emu::prepare(args.into(), receiver);
let (output, stream) = audio::create_output(mute);
let window_manager = WinitWindowManager::new(sender, stream, record);
gb_emu::run(prepared, window_manager, output);
let mut window_manager = WinitWindowManager::new(sender, stream, record);
let mut core = gb_emu::run(prepared, &mut window_manager, output);
if debug {
let mut debugger = Debugger::new(Box::new(core));
let mut since = Instant::now();
loop {
if since.elapsed() >= UPDATE_INTERVAL {
window_manager.update_events();
since = Instant::now();
}
debugger.step();
}
} else {
std::thread::spawn(move || loop {
if core.run(100) {
exit(0);
}
});
window_manager.run_events_blocking().unwrap();
}
}
}
const UPDATE_INTERVAL: Duration = Duration::from_millis(1);

View file

@ -1,6 +1,118 @@
use gb_emu::window::{RendererChannel, WindowManager};
use std::{
collections::HashMap,
path::PathBuf,
sync::{
mpsc::{self, Receiver, Sender},
Arc,
},
thread::JoinHandle,
};
pub struct CacaoWindowManager {}
use cacao::{
appkit::window::{Window, WindowConfig, WindowDelegate, WindowStyle},
core_foundation::base::TCFTypeRef,
};
use cpal::Stream;
use gb_emu::window::{RendererChannel, WindowManager};
use gb_emu_lib::{
connect::{EmulatorMessage, RendererMessage, ResolutionData},
renderer::{RendererBackend, RendererBackendManager, WindowOptions},
};
use raw_window_handle::{
AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle,
RawDisplayHandle, RawWindowHandle,
};
use uuid::Uuid;
use super::{dispatch, AppMessage};
pub enum EmuWindowMessage {
Closing,
Renderer(RendererMessage<[u8; 4]>),
}
pub struct EmulatorHandles {
sender: Sender<EmulatorMessage>,
emulator_thread: Option<JoinHandle<()>>,
_stream: Stream,
}
impl EmulatorHandles {
fn new(sender: Sender<EmulatorMessage>, stream: Stream) -> Self {
Self {
sender,
emulator_thread: None,
_stream: stream,
}
}
pub fn set_thread_handle(&mut self, handle: JoinHandle<()>) {
self.emulator_thread = Some(handle);
}
}
impl Drop for EmulatorHandles {
fn drop(&mut self) {
if let Some(handle) = self.emulator_thread.take() {
self.sender.send(EmulatorMessage::Exit).unwrap();
handle.join().unwrap();
}
}
}
pub struct CacaoWindowManager {
backend_manager: Arc<RendererBackendManager>,
windows: HashMap<Uuid, Window<CacaoWindow>>,
handles: Option<EmulatorHandles>,
}
impl CacaoWindowManager {
pub fn new(display_handle: RawDisplayHandle) -> Self {
Self {
backend_manager: Arc::new(RendererBackendManager::new(display_handle)),
windows: HashMap::new(),
handles: None,
}
}
pub fn is_emulator_running(&self) -> bool {
self.handles.is_some()
}
pub fn update_handles(&mut self, sender: Sender<EmulatorMessage>, stream: Stream) {
self.handles = Some(EmulatorHandles::new(sender, stream));
}
pub fn set_thread_handle(&mut self, handle: JoinHandle<()>) {
if let Some(ref mut handles) = self.handles {
handles.set_thread_handle(handle);
}
}
pub fn message(&mut self, id: Uuid, message: EmuWindowMessage) {
match message {
EmuWindowMessage::Closing => {
self.windows.remove(&id);
if self.windows.is_empty() {
let _ = self.handles.take();
}
}
EmuWindowMessage::Renderer(message) => {
if let Some(window) = self.windows.get_mut(&id) {
if let Some(delegate) = window.delegate.as_mut() {
delegate.message(
message,
Window {
delegate: None,
objc: window.objc.clone(),
},
);
}
}
}
}
}
}
impl WindowManager for CacaoWindowManager {
fn add_main(
@ -9,7 +121,7 @@ impl WindowManager for CacaoWindowManager {
shader_path: Option<std::path::PathBuf>,
resizable: bool,
) -> RendererChannel {
todo!()
self.add(factor, shader_path, resizable)
}
fn add(
@ -18,14 +130,192 @@ impl WindowManager for CacaoWindowManager {
shader_path: Option<std::path::PathBuf>,
resizable: bool,
) -> RendererChannel {
todo!()
}
let (w, receiver) = CacaoWindow::new(factor, shader_path, self.backend_manager.clone());
let window = Window::with(
{
let mut config = WindowConfig::default();
config.set_initial_dimensions(0., 0., 800., 800.);
fn update_events(&mut self) {
todo!()
}
let mut styles = vec![
WindowStyle::Miniaturizable,
WindowStyle::Closable,
WindowStyle::Titled,
];
if resizable {
styles.push(WindowStyle::Resizable);
}
fn run_events_blocking(self) -> Result<(), winit::error::EventLoopError> {
todo!()
config.set_styles(&styles);
config
},
w,
);
window.show();
self.windows
.insert(window.delegate.as_ref().unwrap().id, window);
receiver
}
}
pub struct MonitorThread {
state: MonitorThreadState,
}
enum MonitorThreadState {
Live {
thread: JoinHandle<()>,
destroy: Sender<bool>,
},
Destroyed,
}
impl MonitorThread {
fn new(receiver: Receiver<RendererMessage<[u8; 4]>>, id: Uuid) -> Self {
let (destroy, destroy_recv) = mpsc::channel();
let thread = std::thread::spawn(move || loop {
if let Ok(message) = receiver.recv() {
dispatch(AppMessage::EmuWindow {
id,
message: EmuWindowMessage::Renderer(message),
})
}
if let Ok(true) = destroy_recv.try_recv() {
return;
}
});
Self {
state: MonitorThreadState::Live { thread, destroy },
}
}
}
impl MonitorThreadState {
fn destroy(&mut self) {
if let Self::Live { thread, destroy } = std::mem::replace(self, Self::Destroyed) {
destroy.send(true).expect("Failed to kill monitor thread");
thread.join().unwrap();
}
}
}
impl Drop for MonitorThread {
fn drop(&mut self) {
self.state.destroy();
}
}
struct CacaoWindow {
id: Uuid,
backend_manager: Arc<RendererBackendManager>,
backend: Option<RendererBackend>,
scale_factor: usize,
shader_path: Option<PathBuf>,
resolutions: ResolutionData,
_channel_thread: MonitorThread,
}
impl CacaoWindow {
fn new(
scale_factor: usize,
shader_path: Option<std::path::PathBuf>,
backend_manager: Arc<RendererBackendManager>,
) -> (Self, RendererChannel) {
let id = Uuid::new_v4();
let (sender, receiver) = mpsc::channel();
let channel_thread = MonitorThread::new(receiver, id);
(
Self {
id,
backend_manager,
backend: None,
scale_factor,
shader_path,
resolutions: ResolutionData {
real_width: 1,
real_height: 1,
scaled_width: 1,
scaled_height: 1,
},
_channel_thread: channel_thread,
},
sender,
)
}
fn message(&mut self, message: RendererMessage<[u8; 4]>, window: Window) {
match message {
RendererMessage::Prepare { width, height } => self.resize(width, height, window),
RendererMessage::Resize { width, height } => self.resize(width, height, window),
RendererMessage::Display { buffer } => self.display(buffer),
RendererMessage::SetTitle { title } => window.set_title(&title),
RendererMessage::Rumble { rumble: _ } => todo!(),
}
}
fn resize(&mut self, width: usize, height: usize, window: Window) {
window.set_content_size(
(width * self.scale_factor) as f64,
(height * self.scale_factor) as f64,
);
let real_factor = (window.backing_scale_factor() * self.scale_factor as f64) as u32;
let (width, height) = (width as u32, height as u32);
self.resolutions = ResolutionData {
real_width: width * real_factor,
real_height: height * real_factor,
scaled_width: width,
scaled_height: height,
}
}
fn display(&mut self, buffer: Vec<[u8; 4]>) {
if let Some(backend) = self.backend.as_mut() {
backend.render(self.resolutions, &self.backend_manager);
backend.new_frame(&buffer);
}
}
}
impl WindowDelegate for CacaoWindow {
const NAME: &'static str = "EmulatorWindow";
fn did_load(&mut self, window: Window) {
#[cfg(feature = "vulkan")]
let options = WindowOptions {
shader_path: self.shader_path.clone(),
};
#[cfg(feature = "pixels")]
let options = WindowOptions {};
self.backend = Some(RendererBackend::new(
self.resolutions,
&WindowHandleWrapper(&window),
options,
self.backend_manager.clone(),
));
}
fn will_close(&self) {
dispatch(AppMessage::EmuWindow {
id: self.id,
message: EmuWindowMessage::Closing,
})
}
}
struct WindowHandleWrapper<'a, T>(&'a Window<T>);
unsafe impl<'a, T> HasRawDisplayHandle for WindowHandleWrapper<'a, T> {
fn raw_display_handle(&self) -> RawDisplayHandle {
RawDisplayHandle::AppKit(AppKitDisplayHandle::empty())
}
}
unsafe impl<'a, T> HasRawWindowHandle for WindowHandleWrapper<'a, T> {
fn raw_window_handle(&self) -> RawWindowHandle {
let Self(w) = self;
let mut handle = AppKitWindowHandle::empty();
handle.ns_window = objc::rc::Id::as_ptr(&w.objc).as_void_ptr().cast_mut();
RawWindowHandle::AppKit(handle)
}
}

View file

@ -8,9 +8,11 @@ use cacao::appkit::{App, AppDelegate};
use cacao::filesystem::FileSelectPanel;
use cacao::notification_center::Dispatcher;
use gb_emu::audio;
use gb_emu_lib::connect::EmulatorMessage;
use gb_emu_lib::connect::{EmulatorCoreTrait, EmulatorMessage};
use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle};
use uuid::Uuid;
use self::cacao_window_manager::CacaoWindowManager;
use self::cacao_window_manager::{CacaoWindowManager, EmuWindowMessage};
use self::preferences::{PreferencesMessage, PreferencesUi};
mod cacao_window_manager;
@ -19,39 +21,38 @@ mod preferences;
pub(crate) enum AppMessage {
Core(CoreMessage),
Preferences(PreferencesMessage),
EmuWindow { id: Uuid, message: EmuWindowMessage },
}
pub(crate) enum CoreMessage {
Open,
ShowOpenDialog,
OpenPreferences,
OpenRom(PathBuf),
}
pub(crate) struct TwincUiApp {
preferences: RwLock<Window<PreferencesUi>>,
current_game: RwLock<CacaoWindowManager>,
}
impl TwincUiApp {
fn open_dialog(&self) {
if self.current_game.read().unwrap().is_emulator_running() {
return;
}
let mut file_select_panel = FileSelectPanel::new();
file_select_panel.set_can_choose_directories(false);
file_select_panel.set_can_choose_files(true);
file_select_panel.set_allows_multiple_selection(false);
file_select_panel.show(move |v| {
if let Some(path) = v.first() {
open_file(path.pathbuf());
dispatch(AppMessage::Core(CoreMessage::OpenRom(path.pathbuf())));
}
});
}
}
fn open_file(path: PathBuf) {
let (sender, receiver) = channel::<EmulatorMessage>();
let prepared = gb_emu::prepare(gb_emu::RunOptions::new(path), receiver);
let (output, stream) = audio::create_output(false);
let window_manager = CacaoWindowManager {};
gb_emu::run(prepared, window_manager, output);
}
impl Default for TwincUiApp {
fn default() -> Self {
Self {
@ -72,6 +73,9 @@ impl Default for TwincUiApp {
},
PreferencesUi::new(),
)),
current_game: RwLock::new(CacaoWindowManager::new(RawDisplayHandle::AppKit(
AppKitDisplayHandle::empty(),
))),
}
}
}
@ -88,10 +92,28 @@ impl Dispatcher for TwincUiApp {
fn on_ui_message(&self, message: Self::Message) {
match message {
AppMessage::Core(CoreMessage::Open) => self.open_dialog(),
AppMessage::Core(CoreMessage::ShowOpenDialog) => self.open_dialog(),
AppMessage::Core(CoreMessage::OpenPreferences) => {
self.preferences.read().unwrap().show();
}
AppMessage::Core(CoreMessage::OpenRom(path)) => {
let (sender, receiver) = channel::<EmulatorMessage>();
sender.send(EmulatorMessage::Start).unwrap();
let prepared = gb_emu::prepare(gb_emu::RunOptions::new(path), receiver);
let (output, stream) = audio::create_output(false);
let mut window_manager = self.current_game.write().unwrap();
window_manager.update_handles(sender, stream);
let mut core = gb_emu::run(prepared, &mut *window_manager, output);
let handle = std::thread::Builder::new()
.name(String::from("EmuCore"))
.spawn(move || loop {
if core.run(100) {
break;
}
})
.unwrap();
window_manager.set_thread_handle(handle);
}
AppMessage::Preferences(prefs_message) => {
if let Ok(mut prefs) = self.preferences.write() {
if let Some(ref mut delegate) = prefs.delegate {
@ -99,6 +121,11 @@ impl Dispatcher for TwincUiApp {
}
}
}
AppMessage::EmuWindow { id, message } => {
if let Ok(mut window_manager) = self.current_game.write() {
window_manager.message(id, message);
}
}
}
}
}
@ -127,7 +154,7 @@ fn menu() -> Vec<Menu> {
"File",
vec![MenuItem::new("Open")
.key("o")
.action(|| dispatch(AppMessage::Core(CoreMessage::Open)))],
.action(|| dispatch(AppMessage::Core(CoreMessage::ShowOpenDialog)))],
),
Menu::new(
"Window",

View file

@ -2,12 +2,12 @@
#[cfg(feature = "camera")]
use camera::Webcam;
use debug::Debugger;
use gb_emu_lib::{
config::{ConfigManager, NamedConfig},
connect::{
AudioOutput, CameraWrapper, CgbRomType, EmulatorCoreTrait, EmulatorMessage,
EmulatorOptions, NoCamera, Rom, RomFile, SerialTarget, SramType,
AudioOutput, CameraWrapper, CgbRomType, EmulatorMessage, EmulatorOptions, NoCamera, Rom,
RomFile, SerialTarget, SramType,
},
EmulatorCore,
};
@ -15,7 +15,6 @@ use serde::{Deserialize, Serialize};
use std::{
path::PathBuf,
sync::{mpsc::Receiver, Arc, Mutex, OnceLock},
time::{Duration, Instant},
};
use window::WindowManager;
@ -109,11 +108,8 @@ where
serial: SerialTarget,
tile_window: bool,
layer_window: bool,
debug: bool,
}
pub fn init_configs() {}
pub fn prepare(
options: RunOptions,
receiver: Receiver<EmulatorMessage>,
@ -166,17 +162,19 @@ pub fn prepare(
shader_path,
resizable,
rom,
receiver,
camera,
serial: options.serial,
tile_window: options.tile_window,
layer_window: options.layer_window,
debug: options.debug,
}
}
pub fn run<W>(prepared: PreparedEmulator<NoCamera>, mut window_manager: W, output: AudioOutput)
pub fn run<W>(
prepared: PreparedEmulator<NoCamera>,
window_manager: &mut W,
output: AudioOutput,
) -> EmulatorCore<[u8; 4], NoCamera>
where
W: WindowManager,
{
@ -229,25 +227,5 @@ where
// EmulatorTypes::Normal(core)
// };
let mut core = EmulatorCore::init(true, prepared.receiver, emulator_options, prepared.camera);
if prepared.debug {
let mut debugger = Debugger::new(Box::new(core));
let mut since = Instant::now();
loop {
if since.elapsed() >= UPDATE_INTERVAL {
window_manager.update_events();
since = Instant::now();
}
debugger.step();
}
} else {
std::thread::spawn(move || loop {
core.run(100);
});
window_manager.run_events_blocking().unwrap();
}
EmulatorCore::init(true, prepared.receiver, emulator_options, prepared.camera)
}
const UPDATE_INTERVAL: Duration = Duration::from_millis(1);

View file

@ -19,6 +19,4 @@ pub trait WindowManager {
shader_path: Option<PathBuf>,
resizable: bool,
) -> RendererChannel;
fn update_events(&mut self);
fn run_events_blocking(self) -> Result<(), winit::error::EventLoopError>;
}

View file

@ -70,6 +70,16 @@ impl WinitWindowManager {
record_main,
}
}
pub fn update_events(&mut self) {
self.event_loop
.pump_events(None, |event, target| self.data.handler(true, event, target));
}
pub fn run_events_blocking(mut self) -> Result<(), winit::error::EventLoopError> {
self.event_loop
.run(move |event, target| self.data.handler(false, event, target))
}
}
impl WindowManager for WinitWindowManager {
@ -104,16 +114,6 @@ impl WindowManager for WinitWindowManager {
false,
)
}
fn update_events(&mut self) {
self.event_loop
.pump_events(None, |event, target| self.data.handler(true, event, target));
}
fn run_events_blocking(mut self) -> Result<(), winit::error::EventLoopError> {
self.event_loop
.run(move |event, target| self.data.handler(false, event, target))
}
}
impl WinitWindowManagerData {

View file

@ -76,6 +76,7 @@ impl RomFile {
}
}
#[derive(Debug)]
pub enum RendererMessage<Format: From<Colour>> {
Prepare { width: usize, height: usize },
Resize { width: usize, height: usize },
@ -354,7 +355,7 @@ pub trait EmulatorCoreTrait {
fn pc(&self) -> u16;
fn print_reg(&self) -> String;
fn get_memory(&self, address: u16) -> u8;
fn run(&mut self, cycles: usize);
fn run(&mut self, cycles: usize) -> bool;
fn run_until_buffer_full(&mut self);
fn process_messages(&mut self);
fn process_messages(&mut self) -> bool;
}

View file

@ -11,7 +11,6 @@ use processor::{
};
use std::{
marker::PhantomData,
process::exit,
sync::{mpsc::Receiver, Arc, Mutex},
};
@ -151,23 +150,30 @@ where
self.cpu.exec_next();
}
fn process_messages(&mut self) {
fn process_messages(&mut self) -> bool {
while let Ok(msg) = self.receiver.try_recv() {
self.process_message(msg);
if self.process_message(msg) {
return true;
}
}
while self.paused {
match self.receiver.recv() {
Ok(msg) => self.process_message(msg),
Ok(msg) => {
if self.process_message(msg) {
return true;
}
}
Err(e) => panic!("no message sender! error {e:#?}"),
}
}
false
}
fn process_message(&mut self, msg: EmulatorMessage) {
fn process_message(&mut self, msg: EmulatorMessage) -> bool {
match msg {
EmulatorMessage::Exit => {
self.cpu.memory.flush_rom();
exit(0);
return true;
}
EmulatorMessage::Start => self.paused = false,
EmulatorMessage::Pause => self.paused = true,
@ -175,6 +181,7 @@ where
self.cpu.next_joypad_state = Some(new_state)
}
}
false
}
}
@ -214,23 +221,28 @@ where
self.cpu.memory.get(address)
}
fn run(&mut self, cycles: usize) {
self.process_messages();
fn run(&mut self, cycles: usize) -> bool {
if self.process_messages() {
return true;
}
if !self.paused {
for _ in 0..cycles {
self.run_cycle();
}
}
false
}
fn run_until_buffer_full(&mut self) {
self.process_messages();
if self.process_messages() {
return;
}
while !self.cpu.memory.is_audio_buffer_full() {
self.run_cycle();
}
}
fn process_messages(&mut self) {
self.process_messages();
fn process_messages(&mut self) -> bool {
self.process_messages()
}
}