tile/layer window in menu / also closable

This commit is contained in:
Alex Janka 2023-11-28 16:21:15 +11:00
parent 6d5530efe6
commit 1b62211b90
13 changed files with 268 additions and 109 deletions

3
Cargo.lock generated
View file

@ -487,7 +487,7 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "cacao"
version = "0.4.0-beta2"
source = "git+https://github.com/italicsjenga/cacao#4fd93e3faeec5e80aef36779745b3506c9b5017d"
source = "git+https://github.com/italicsjenga/cacao#65b32e79c0a8d088d19799b13e86634f3a64c10b"
dependencies = [
"bitmask-enum",
"block2 0.2.0-alpha.6",
@ -1639,6 +1639,7 @@ dependencies = [
"gb-emu-lib",
"objc2 0.3.0-beta.3",
"raw-window-handle 0.5.2",
"serde",
"twinc_emu_vst",
"uuid 1.6.1",
]

View file

@ -158,7 +158,7 @@ fn main() {
}
}
} else {
let (sender, receiver) = channel::<EmulatorMessage>();
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
{
let sender = sender.clone();

View file

@ -16,7 +16,7 @@ use std::{
path::PathBuf,
sync::{mpsc::Receiver, Arc, Mutex, OnceLock},
};
use window::WindowManager;
use window::{RendererChannel, WindowManager, WindowType};
pub mod audio;
#[cfg(feature = "camera")]
@ -103,7 +103,7 @@ where
shader_path: Option<PathBuf>,
resizable: bool,
rom: Rom<C>,
receiver: Receiver<EmulatorMessage>,
receiver: Receiver<EmulatorMessage<[u8; 4]>>,
camera: Arc<Mutex<CameraWrapper<C>>>,
serial: SerialTarget,
tile_window: bool,
@ -112,7 +112,7 @@ where
pub fn prepare(
options: RunOptions,
receiver: Receiver<EmulatorMessage>,
receiver: Receiver<EmulatorMessage<[u8; 4]>>,
) -> PreparedEmulator<NoCamera> {
let config = CONFIG_MANAGER.load_or_create_base_config();
let standalone_config: StandaloneConfig = CONFIG_MANAGER.load_or_create_config();
@ -180,21 +180,21 @@ where
{
let configs = access_config();
let window =
window_manager.add_main(
prepared.scale_override,
prepared.shader_path,
prepared.resizable,
);
let window = window_manager.add(
WindowType::Main,
prepared.scale_override,
prepared.shader_path,
prepared.resizable,
);
let tile_window = if prepared.tile_window {
Some(window_manager.add(configs.standalone_config.scale_factor, None, false))
Some(new_tile_window(window_manager))
} else {
None
};
let layer_window = if prepared.layer_window {
Some(window_manager.add(configs.standalone_config.scale_factor.min(2), None, false))
Some(new_layer_window(window_manager))
} else {
None
};
@ -230,3 +230,27 @@ where
EmulatorCore::init(true, prepared.receiver, emulator_options, prepared.camera)
}
pub fn new_tile_window<W>(window_manager: &mut W) -> RendererChannel
where
W: WindowManager,
{
window_manager.add(
WindowType::Tile,
access_config().standalone_config.scale_factor,
None,
false,
)
}
pub fn new_layer_window<W>(window_manager: &mut W) -> RendererChannel
where
W: WindowManager,
{
window_manager.add(
WindowType::Layer,
access_config().standalone_config.scale_factor.min(2),
None,
false,
)
}

View file

@ -6,15 +6,17 @@ pub mod winit_manager;
pub type RendererChannel = Sender<RendererMessage<[u8; 4]>>;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum WindowType {
Main,
Tile,
Layer,
}
pub trait WindowManager {
fn add_main(
&mut self,
factor: usize,
shader_path: Option<PathBuf>,
resizable: bool,
) -> RendererChannel;
fn add(
&mut self,
window_type: WindowType,
factor: usize,
shader_path: Option<PathBuf>,
resizable: bool,

View file

@ -28,7 +28,7 @@ use winit_input_helper::WinitInputHelper;
use crate::access_config;
use super::{RendererChannel, WindowManager};
use super::{RendererChannel, WindowManager, WindowType};
pub struct WinitWindowManager {
event_loop: EventLoop<()>,
@ -41,14 +41,18 @@ struct WinitWindowManagerData {
windows: HashMap<WindowId, WindowRenderer>,
window_data_manager: Arc<RendererBackendManager>,
input: WinitInputHelper,
sender: Sender<EmulatorMessage>,
sender: Sender<EmulatorMessage<[u8; 4]>>,
gamepad_handler: Gilrs,
joypad_state: JoypadState,
_stream: Stream,
}
impl WinitWindowManager {
pub fn new(sender: Sender<EmulatorMessage>, _stream: Stream, record_main: bool) -> Self {
pub fn new(
sender: Sender<EmulatorMessage<[u8; 4]>>,
_stream: Stream,
record_main: bool,
) -> Self {
let event_loop = EventLoop::new().unwrap();
#[cfg(feature = "vulkan")]
let window_data_manager =
@ -83,35 +87,22 @@ impl WinitWindowManager {
}
impl WindowManager for WinitWindowManager {
fn add_main(
&mut self,
factor: usize,
shader_path: Option<PathBuf>,
resizable: bool,
) -> RendererChannel {
self.data.add(
factor,
shader_path,
resizable,
&self.event_loop,
true,
self.record_main,
)
}
fn add(
&mut self,
window_type: WindowType,
factor: usize,
shader_path: Option<PathBuf>,
resizable: bool,
) -> RendererChannel {
let is_main = window_type == WindowType::Main;
self.data.add(
factor,
shader_path,
resizable,
&self.event_loop,
false,
false,
is_main,
is_main && self.record_main,
)
}
}

View file

@ -57,7 +57,7 @@ struct EmuVars {
}
struct EmuComms {
sender: Sender<EmulatorMessage>,
sender: Sender<EmulatorMessage<[u8; 4]>>,
receiver: Receiver<RendererMessage<[u8; 4]>>,
}
@ -288,7 +288,7 @@ impl Plugin for GameboyEmu {
let _ =
IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb);
let (sender, receiver) = channel::<EmulatorMessage>();
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
let (output, rx) = AudioOutput::new(
buffer_config.sample_rate,

View file

@ -21,3 +21,4 @@ objc = { version = "=0.3.0-beta.3", package = "objc2" }
uuid = { version = "1.6", features = ["v4", "fast-rng"] }
raw-window-handle = { version = "0.5" }
cpal = "0.15"
serde = { version = "1.0", features = ["derive"] }

View file

@ -16,7 +16,10 @@ use cacao::{
core_foundation::base::TCFTypeRef,
};
use cpal::Stream;
use frontend_common::window::{RendererChannel, WindowManager};
use frontend_common::{
new_layer_window, new_tile_window,
window::{RendererChannel, WindowManager, WindowType},
};
use gb_emu_lib::{
connect::{EmulatorMessage, JoypadButtons, JoypadState, RendererMessage, ResolutionData},
renderer::{RendererBackend, RendererBackendManager, WindowOptions},
@ -25,16 +28,15 @@ use raw_window_handle::{
AppKitDisplayHandle, AppKitWindowHandle, HasRawDisplayHandle, HasRawWindowHandle,
RawDisplayHandle, RawWindowHandle,
};
use uuid::Uuid;
use super::{dispatch, AppMessage};
use super::{dispatch, AppMessage, CoreMessage};
pub enum EmuWindowMessage {
Closing {
id: Uuid,
window_type: WindowType,
},
Renderer {
id: Uuid,
window_type: WindowType,
renderer_message: RendererMessage<[u8; 4]>,
},
KeyDown(JoypadButtons),
@ -42,13 +44,13 @@ pub enum EmuWindowMessage {
}
pub struct EmulatorHandles {
sender: Sender<EmulatorMessage>,
sender: Sender<EmulatorMessage<[u8; 4]>>,
emulator_thread: Option<JoinHandle<()>>,
_stream: Stream,
}
impl EmulatorHandles {
fn new(sender: Sender<EmulatorMessage>, stream: Stream) -> Self {
fn new(sender: Sender<EmulatorMessage<[u8; 4]>>, stream: Stream) -> Self {
Self {
sender,
emulator_thread: None,
@ -149,7 +151,7 @@ fn get_buttons(event: &Event) -> Option<JoypadButtons> {
pub struct CacaoWindowManager {
backend_manager: Arc<RendererBackendManager>,
windows: HashMap<Uuid, Window<CacaoWindow>>,
windows: HashMap<WindowType, Window<CacaoWindow>>,
handles: Option<EmulatorHandles>,
joypad_state: JoypadState,
_button_handler: ButtonHandler,
@ -172,7 +174,7 @@ impl CacaoWindowManager {
self.handles.is_some()
}
pub fn update_handles(&mut self, sender: Sender<EmulatorMessage>, stream: Stream) {
pub fn update_handles(&mut self, sender: Sender<EmulatorMessage<[u8; 4]>>, stream: Stream) {
self.handles = Some(EmulatorHandles::new(sender, stream));
}
@ -184,17 +186,32 @@ impl CacaoWindowManager {
pub fn message(&mut self, message: EmuWindowMessage) {
match message {
EmuWindowMessage::Closing { id } => {
self.windows.remove(&id);
if self.windows.is_empty() {
let _ = self.handles.take();
}
EmuWindowMessage::Closing { window_type } => {
self.windows.remove(&window_type);
match window_type {
WindowType::Main => {
let _ = self.handles.take();
for (_, w) in self.windows.drain() {
w.close();
}
}
WindowType::Tile => {
if self.is_emulator_running() {
dispatch(AppMessage::Core(CoreMessage::SetTileWindow(false)))
}
}
WindowType::Layer => {
if self.is_emulator_running() {
dispatch(AppMessage::Core(CoreMessage::SetLayerWindow(false)))
}
}
};
}
EmuWindowMessage::Renderer {
id,
window_type,
renderer_message,
} => {
if let Some(window) = self.windows.get_mut(&id) {
if let Some(window) = self.windows.get_mut(&window_type) {
if let Some(delegate) = window.delegate.as_mut() {
delegate.message(
renderer_message,
@ -232,25 +249,52 @@ impl CacaoWindowManager {
}
}
}
pub fn set_layer_window(&mut self, state: bool) {
if state {
let is_running = self.is_emulator_running();
if is_running {
let new_layer_window = new_layer_window(self);
if let Some(ref handles) = self.handles {
handles
.sender
.send(EmulatorMessage::NewLayerWindow(new_layer_window))
.unwrap();
}
}
}
}
pub fn set_tile_window(&mut self, state: bool) {
if state {
let is_running = self.is_emulator_running();
if is_running {
let new_tile_window = new_tile_window(self);
if let Some(ref handles) = self.handles {
handles
.sender
.send(EmulatorMessage::NewTileWindow(new_tile_window))
.unwrap();
}
}
}
}
}
impl WindowManager for CacaoWindowManager {
fn add_main(
&mut self,
factor: usize,
shader_path: Option<std::path::PathBuf>,
resizable: bool,
) -> RendererChannel {
self.add(factor, shader_path, resizable)
}
fn add(
&mut self,
window_type: WindowType,
factor: usize,
shader_path: Option<std::path::PathBuf>,
resizable: bool,
) -> RendererChannel {
let (w, receiver) = CacaoWindow::new(factor, shader_path, self.backend_manager.clone());
let (w, receiver) = CacaoWindow::new(
window_type,
factor,
shader_path,
self.backend_manager.clone(),
);
let window = Window::with(
{
let mut config = WindowConfig::default();
@ -272,7 +316,7 @@ impl WindowManager for CacaoWindowManager {
);
window.show();
self.windows
.insert(window.delegate.as_ref().unwrap().id, window);
.insert(window.delegate.as_ref().unwrap().window_type, window);
receiver
}
}
@ -290,20 +334,19 @@ enum MonitorThreadState {
}
impl MonitorThread {
fn new(receiver: Receiver<RendererMessage<[u8; 4]>>, id: Uuid) -> Self {
fn new(receiver: Receiver<RendererMessage<[u8; 4]>>, window_type: WindowType) -> Self {
let (destroy, destroy_recv) = mpsc::channel();
let thread =
std::thread::spawn(move || loop {
if let Ok(renderer_message) = receiver.recv() {
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Renderer {
id,
renderer_message,
}));
}
if let Ok(true) = destroy_recv.try_recv() {
return;
}
});
let thread = std::thread::spawn(move || loop {
if let Ok(renderer_message) = receiver.recv() {
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Renderer {
window_type,
renderer_message,
}));
}
if let Ok(true) = destroy_recv.try_recv() {
return;
}
});
Self {
state: MonitorThreadState::Live { thread, destroy },
}
@ -326,7 +369,7 @@ impl Drop for MonitorThread {
}
struct CacaoWindow {
id: Uuid,
window_type: WindowType,
backend_manager: Arc<RendererBackendManager>,
backend: Option<RendererBackend>,
scale_factor: usize,
@ -337,16 +380,16 @@ struct CacaoWindow {
impl CacaoWindow {
fn new(
window_type: WindowType,
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);
let channel_thread = MonitorThread::new(receiver, window_type);
(
Self {
id,
window_type,
backend_manager,
backend: None,
scale_factor,
@ -413,7 +456,9 @@ impl WindowDelegate for CacaoWindow {
}
fn will_close(&self) {
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Closing { id: self.id }));
dispatch(AppMessage::EmuWindow(EmuWindowMessage::Closing {
window_type: self.window_type,
}));
}
}

View file

@ -8,8 +8,10 @@ use cacao::appkit::{App, AppDelegate};
use cacao::filesystem::FileSelectPanel;
use cacao::notification_center::Dispatcher;
use frontend_common::audio;
use gb_emu_lib::config::{NamedConfig, CONFIG_MANAGER};
use gb_emu_lib::connect::{EmulatorCoreTrait, EmulatorMessage};
use raw_window_handle::{AppKitDisplayHandle, RawDisplayHandle};
use serde::{Deserialize, Serialize};
use self::cacao_window_manager::{CacaoWindowManager, EmuWindowMessage};
use self::preferences::{PreferencesMessage, PreferencesUi};
@ -25,13 +27,31 @@ pub(crate) enum AppMessage {
pub(crate) enum CoreMessage {
ShowOpenDialog,
ToggleTileWindow,
ToggleLayerWindow,
SetTileWindow(bool),
SetLayerWindow(bool),
OpenPreferences,
OpenRom(PathBuf),
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[serde(default)]
pub struct MacGuiConfig {
pub tile_window: bool,
pub layer_window: bool,
}
impl NamedConfig for MacGuiConfig {
fn name() -> String {
String::from("mac-gui")
}
}
pub(crate) struct TwincUiApp {
preferences: RwLock<Window<PreferencesUi>>,
current_game: RwLock<CacaoWindowManager>,
gui_config: RwLock<MacGuiConfig>,
}
impl TwincUiApp {
@ -75,13 +95,14 @@ impl Default for TwincUiApp {
current_game: RwLock::new(
CacaoWindowManager::new(RawDisplayHandle::AppKit(AppKitDisplayHandle::empty()))
),
gui_config: RwLock::new(CONFIG_MANAGER.load_or_create_config()),
}
}
}
impl AppDelegate for TwincUiApp {
fn did_finish_launching(&self) {
App::set_menu(menu());
App::set_menu(menu(&self.gui_config.read().unwrap()));
App::activate();
}
}
@ -95,11 +116,57 @@ impl Dispatcher for TwincUiApp {
AppMessage::Core(CoreMessage::OpenPreferences) => {
self.preferences.read().unwrap().show();
}
AppMessage::Core(CoreMessage::ToggleLayerWindow) => {
if let Ok(mut config) = self.gui_config.write() {
config.layer_window = !config.layer_window;
App::set_menu(menu(&config));
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
if let Ok(mut current_game) = self.current_game.write() {
current_game.set_layer_window(config.layer_window);
}
}
}
AppMessage::Core(CoreMessage::ToggleTileWindow) => {
if let Ok(mut config) = self.gui_config.write() {
config.tile_window = !config.tile_window;
App::set_menu(menu(&config));
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
if let Ok(mut current_game) = self.current_game.write() {
current_game.set_tile_window(config.tile_window);
}
}
}
AppMessage::Core(CoreMessage::SetLayerWindow(val)) => {
if let Ok(mut config) = self.gui_config.write() {
config.layer_window = val;
App::set_menu(menu(&config));
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
if let Ok(mut current_game) = self.current_game.write() {
current_game.set_layer_window(config.layer_window);
}
}
}
AppMessage::Core(CoreMessage::SetTileWindow(val)) => {
if let Ok(mut config) = self.gui_config.write() {
config.tile_window = val;
App::set_menu(menu(&config));
let _ = CONFIG_MANAGER.save_custom_config(config.clone());
if let Ok(mut current_game) = self.current_game.write() {
current_game.set_tile_window(config.tile_window);
}
}
}
AppMessage::Core(CoreMessage::OpenRom(path)) => {
let (sender, receiver) = channel::<EmulatorMessage>();
let (sender, receiver) = channel::<EmulatorMessage<[u8; 4]>>();
sender.send(EmulatorMessage::Start).unwrap();
let prepared =
frontend_common::prepare(frontend_common::RunOptions::new(path), receiver);
let mut options = frontend_common::RunOptions::new(path);
if let Ok(config) = self.gui_config.read() {
options.layer_window = config.layer_window;
options.tile_window = config.tile_window
}
let prepared = frontend_common::prepare(options, receiver);
let (output, stream) = audio::create_output(false);
let mut window_manager = self.current_game.write().unwrap();
window_manager.update_handles(sender, stream);
@ -130,7 +197,7 @@ impl Dispatcher for TwincUiApp {
}
}
fn menu() -> Vec<Menu> {
fn menu(config: &MacGuiConfig) -> Vec<Menu> {
vec![
Menu::new(
"",
@ -159,6 +226,13 @@ fn menu() -> Vec<Menu> {
Menu::new(
"Window",
vec![
MenuItem::new("Tiles")
.checkmark(config.tile_window)
.action(|| dispatch(AppMessage::Core(CoreMessage::ToggleTileWindow))),
MenuItem::new("Layers")
.checkmark(config.layer_window)
.action(|| dispatch(AppMessage::Core(CoreMessage::ToggleLayerWindow))),
MenuItem::Separator,
MenuItem::Minimize,
MenuItem::Separator,
MenuItem::new("Bring All to Front"),

View file

@ -14,11 +14,16 @@ pub use crate::{HEIGHT, WIDTH};
use async_ringbuf::{AsyncHeapConsumer, AsyncHeapProducer, AsyncHeapRb};
#[derive(Debug)]
pub enum EmulatorMessage {
pub enum EmulatorMessage<ColourFormat>
where
ColourFormat: From<Colour> + Copy,
{
Start,
Pause,
Exit,
JoypadUpdate(JoypadState),
NewLayerWindow(Sender<RendererMessage<ColourFormat>>),
NewTileWindow(Sender<RendererMessage<ColourFormat>>),
}
#[derive(Clone, Copy)]

View file

@ -38,7 +38,7 @@ where
ColourFormat: From<Colour> + Copy,
C: PocketCamera + Send + 'static,
{
receiver: Receiver<EmulatorMessage>,
receiver: Receiver<EmulatorMessage<ColourFormat>>,
cpu: Cpu<ColourFormat, C>,
paused: bool,
spooky: PhantomData<C>,
@ -51,7 +51,7 @@ where
{
pub fn init(
paused: bool,
receiver: Receiver<EmulatorMessage>,
receiver: Receiver<EmulatorMessage<ColourFormat>>,
options: EmulatorOptions<ColourFormat, C>,
camera: Arc<Mutex<CameraWrapper<C>>>,
) -> Self {
@ -59,13 +59,13 @@ where
let is_cgb_mode = rom.rom_type == CgbRomType::CgbOnly || options.cgb_mode;
let bootrom = if is_cgb_mode {
options.cgb_bootrom.unwrap_or(RomFile::Raw(
include_bytes!("../../sameboy-bootroms/cgb_boot.bin").to_vec(),
))
options.cgb_bootrom.unwrap_or(
RomFile::Raw(include_bytes!("../../sameboy-bootroms/cgb_boot.bin").to_vec())
)
} else {
options.dmg_bootrom.unwrap_or(RomFile::Raw(
include_bytes!("../../sameboy-bootroms/dmg_boot.bin").to_vec(),
))
options.dmg_bootrom.unwrap_or(
RomFile::Raw(include_bytes!("../../sameboy-bootroms/dmg_boot.bin").to_vec())
)
}
.load_data()
.expect("Error loading bootrom!");
@ -111,7 +111,11 @@ where
)
}
fn new(paused: bool, receiver: Receiver<EmulatorMessage>, cpu: Cpu<ColourFormat, C>) -> Self {
fn new(
paused: bool,
receiver: Receiver<EmulatorMessage<ColourFormat>>,
cpu: Cpu<ColourFormat, C>,
) -> Self {
Self {
receiver,
cpu,
@ -169,7 +173,7 @@ where
false
}
fn process_message(&mut self, msg: EmulatorMessage) -> bool {
fn process_message(&mut self, msg: EmulatorMessage<ColourFormat>) -> bool {
match msg {
EmulatorMessage::Exit => {
self.cpu.memory.flush_rom();
@ -180,6 +184,8 @@ where
EmulatorMessage::JoypadUpdate(new_state) => {
self.cpu.next_joypad_state = Some(new_state)
}
EmulatorMessage::NewLayerWindow(new) => self.cpu.memory.gpu.set_layer_window(new),
EmulatorMessage::NewTileWindow(new) => self.cpu.memory.gpu.set_tile_window(new),
}
false
}

View file

@ -102,7 +102,7 @@ where
pub(super) user_mode: bool,
oam_dma: OamDma,
joypad: Joypad,
gpu: Gpu<ColourFormat>,
pub gpu: Gpu<ColourFormat>,
apu: Apu,
serial: Serial,
timers: Timer,
@ -290,11 +290,13 @@ where
fn get_io(&self, address: IoAddress) -> u8 {
match address {
IoAddress::Joypad => self.joypad.as_register(),
IoAddress::Serial(address) => match address.inner() {
0xFF01 => self.serial.get_queued(),
0xFF02 => self.serial.get_control(),
_ => unreachable!(),
},
IoAddress::Serial(address) => {
match address.inner() {
0xFF01 => self.serial.get_queued(),
0xFF02 => self.serial.get_control(),
_ => unreachable!(),
}
}
IoAddress::Timer(address) => match address.inner() {
0xFF04 => self.timers.get_div(),
0xFF05 => self.timers.get_tima(),

View file

@ -123,6 +123,14 @@ where
}
}
pub fn set_tile_window(&mut self, window: Sender<RendererMessage<ColourFormat>>) {
self.tile_window = Some(TileWindow::new(window, self.cgb_data.is_some()));
}
pub fn set_layer_window(&mut self, window: Sender<RendererMessage<ColourFormat>>) {
self.layer_window = Some(LayerWindow::new(window, self.cgb_data.is_some()));
}
pub(crate) fn get_mode(&self) -> DrawMode {
self.stat.mode
}