some of most settings
This commit is contained in:
parent
59ee750d26
commit
9cfa17ee0e
12 changed files with 676 additions and 429 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1338,6 +1338,7 @@ dependencies = [
|
|||
"raw-window-handle",
|
||||
"send_wrapper",
|
||||
"serde",
|
||||
"twinc_emu_vst",
|
||||
"winit",
|
||||
"winit_input_helper",
|
||||
]
|
||||
|
|
|
@ -40,6 +40,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||
image = { version = "0.24", default-features = false, features = ["png"] }
|
||||
bytemuck = "1.14"
|
||||
chrono = "0.4"
|
||||
twinc_emu_vst = { path = "../gb-vst", default-features = false }
|
||||
|
||||
[target.'cfg(any(target_os = "macos"))'.dependencies]
|
||||
cacao = "0.4.0-beta2"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::sync::RwLock;
|
||||
|
||||
use cacao::appkit::menu::{Menu, MenuItem};
|
||||
use cacao::appkit::window::{Window, WindowConfig, WindowStyle, WindowToolbarStyle};
|
||||
use cacao::appkit::{App, AppDelegate};
|
||||
|
@ -18,16 +20,16 @@ pub(crate) enum CoreMessage {
|
|||
}
|
||||
|
||||
pub(crate) struct TwincUiApp {
|
||||
preferences: Window<PreferencesUi>,
|
||||
preferences: RwLock<Window<PreferencesUi>>,
|
||||
}
|
||||
|
||||
impl Default for TwincUiApp {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
preferences: Window::with(
|
||||
preferences: RwLock::new(Window::with(
|
||||
{
|
||||
let mut config = WindowConfig::default();
|
||||
config.set_initial_dimensions(100., 100., 400., 400.);
|
||||
config.set_initial_dimensions(0., 0., 800., 800.);
|
||||
|
||||
config.set_styles(&[
|
||||
WindowStyle::Resizable,
|
||||
|
@ -40,7 +42,7 @@ impl Default for TwincUiApp {
|
|||
config
|
||||
},
|
||||
PreferencesUi::new(),
|
||||
),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -59,11 +61,13 @@ impl Dispatcher for TwincUiApp {
|
|||
match message {
|
||||
AppMessage::Core(CoreMessage::Open) => println!("open"),
|
||||
AppMessage::Core(CoreMessage::OpenPreferences) => {
|
||||
self.preferences.show();
|
||||
self.preferences.read().unwrap().show();
|
||||
}
|
||||
AppMessage::Preferences(prefs_message) => {
|
||||
if let Some(delegate) = &self.preferences.delegate {
|
||||
delegate.message(prefs_message)
|
||||
if let Ok(mut prefs) = self.preferences.write() {
|
||||
if let Some(ref mut delegate) = prefs.delegate {
|
||||
delegate.message(prefs_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,13 @@ use cacao::{
|
|||
},
|
||||
view::ViewController,
|
||||
};
|
||||
use gb_emu_lib::config::ConfigManager;
|
||||
|
||||
use self::{
|
||||
toolbar::PreferencesToolbar,
|
||||
views::{
|
||||
CorePreferencesContentView, StandalonePreferencesContentView, VstPreferencesContentView,
|
||||
CorePreferencesContentView, CorePreferencesUpdates, StandalonePreferencesContentView,
|
||||
StandalonePreferencesUpdates, VstPreferencesContentView, VstPreferencesUpdates,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -18,6 +20,9 @@ mod views;
|
|||
|
||||
pub(crate) enum PreferencesMessage {
|
||||
SwitchPane(PreferencesPane),
|
||||
UpdateCore(CorePreferencesUpdates),
|
||||
UpdateStandalone(StandalonePreferencesUpdates),
|
||||
UpdateVst(VstPreferencesUpdates),
|
||||
}
|
||||
|
||||
pub(crate) enum PreferencesPane {
|
||||
|
@ -36,31 +41,48 @@ pub(crate) struct PreferencesUi {
|
|||
|
||||
impl PreferencesUi {
|
||||
pub(crate) fn new() -> Self {
|
||||
let config_manager = ConfigManager::get().expect("Couldn't load config dir");
|
||||
Self {
|
||||
toolbar: Toolbar::new("PreferencesToolbar", PreferencesToolbar::default()),
|
||||
core_prefs: ViewController::new(CorePreferencesContentView::default()),
|
||||
standalone_prefs: ViewController::new(StandalonePreferencesContentView::default()),
|
||||
vst_prefs: ViewController::new(VstPreferencesContentView::default()),
|
||||
core_prefs: ViewController::new(CorePreferencesContentView::new(
|
||||
config_manager.clone(),
|
||||
)),
|
||||
standalone_prefs: ViewController::new(StandalonePreferencesContentView::new(
|
||||
config_manager.clone(),
|
||||
)),
|
||||
vst_prefs: ViewController::new(VstPreferencesContentView::new(config_manager)),
|
||||
window: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn message(&self, message: PreferencesMessage) {
|
||||
pub(crate) fn message(&mut self, message: PreferencesMessage) {
|
||||
let window = self.window.as_ref().unwrap();
|
||||
|
||||
match message {
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Core) => {
|
||||
window.set_title("Core");
|
||||
window.set_content_view_controller(&self.core_prefs);
|
||||
}
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Standalone) => {
|
||||
window.set_title("Standalone");
|
||||
window.set_content_view_controller(&self.standalone_prefs);
|
||||
}
|
||||
PreferencesMessage::SwitchPane(PreferencesPane::Vst) => {
|
||||
window.set_title("VST");
|
||||
window.set_content_view_controller(&self.vst_prefs);
|
||||
}
|
||||
PreferencesMessage::UpdateCore(update) => {
|
||||
if let Some(ref mut delegate) = self.core_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
PreferencesMessage::UpdateStandalone(update) => {
|
||||
if let Some(ref mut delegate) = self.standalone_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
PreferencesMessage::UpdateVst(update) => {
|
||||
if let Some(ref mut delegate) = self.vst_prefs.view.delegate {
|
||||
delegate.update(update)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -72,6 +94,7 @@ impl WindowDelegate for PreferencesUi {
|
|||
window.set_autosave_name("PreferencesWindow");
|
||||
window.set_movable_by_background(true);
|
||||
window.set_toolbar(&self.toolbar);
|
||||
window.set_title("Preferences");
|
||||
|
||||
self.window = Some(window);
|
||||
|
||||
|
|
|
@ -1,65 +1,195 @@
|
|||
use cacao::{
|
||||
layout::{Layout, LayoutConstraint},
|
||||
layout::Layout,
|
||||
view::{View, ViewDelegate},
|
||||
};
|
||||
use gb_emu::StandaloneConfig;
|
||||
use gb_emu_lib::config::{Config, ConfigManager};
|
||||
|
||||
use self::widgets::ToggleOptionView;
|
||||
use crate::macos::dispatch;
|
||||
|
||||
use self::widgets::{PathView, ToggleView};
|
||||
|
||||
mod widgets;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct CorePreferencesContentView {
|
||||
pub example_option: ToggleOptionView,
|
||||
config: Config,
|
||||
config_manager: ConfigManager,
|
||||
show_bootrom: ToggleView,
|
||||
prefer_cgb: ToggleView,
|
||||
dmg_bootrom: PathView,
|
||||
}
|
||||
|
||||
impl CorePreferencesContentView {
|
||||
pub(crate) fn new(config_manager: ConfigManager) -> Self {
|
||||
Self {
|
||||
config: config_manager.load_or_create_base_config(),
|
||||
config_manager,
|
||||
show_bootrom: Default::default(),
|
||||
prefer_cgb: Default::default(),
|
||||
dmg_bootrom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: CorePreferencesUpdates) {
|
||||
match update {
|
||||
CorePreferencesUpdates::ShowBootrom => {
|
||||
self.config.show_bootrom = !self.config.show_bootrom
|
||||
}
|
||||
CorePreferencesUpdates::PreferCGB => self.config.prefer_cgb = !self.config.prefer_cgb,
|
||||
}
|
||||
|
||||
self.config_manager
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum CorePreferencesUpdates {
|
||||
ShowBootrom,
|
||||
PreferCGB,
|
||||
}
|
||||
|
||||
impl ViewDelegate for CorePreferencesContentView {
|
||||
const NAME: &'static str = "CorePreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.example_option.configure(
|
||||
"An example preference",
|
||||
"This can be true, or it can be false.",
|
||||
false, // initial value
|
||||
|_v| {},
|
||||
self.show_bootrom
|
||||
.configure("Show BootROM", "", self.config.show_bootrom, |_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::ShowBootrom),
|
||||
))
|
||||
});
|
||||
view.add_subview(&self.show_bootrom.view);
|
||||
|
||||
self.prefer_cgb.configure(
|
||||
"Prefer Game Boy Colour mode",
|
||||
"",
|
||||
self.config.prefer_cgb,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateCore(CorePreferencesUpdates::PreferCGB),
|
||||
))
|
||||
},
|
||||
);
|
||||
view.add_subview(&self.prefer_cgb.view);
|
||||
|
||||
view.add_subview(&self.example_option.view);
|
||||
self.dmg_bootrom
|
||||
.configure("DMG bootrom", self.config.dmg_bootrom.clone());
|
||||
view.add_subview(&self.dmg_bootrom.view);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
self.example_option
|
||||
.view
|
||||
.top
|
||||
.constraint_equal_to(&view.top)
|
||||
.offset(22.),
|
||||
self.example_option
|
||||
.view
|
||||
.leading
|
||||
.constraint_equal_to(&view.leading)
|
||||
.offset(22.),
|
||||
self.example_option
|
||||
.view
|
||||
.trailing
|
||||
.constraint_equal_to(&view.trailing)
|
||||
.offset(-22.),
|
||||
self.example_option
|
||||
.view
|
||||
.bottom
|
||||
.constraint_equal_to(&view.bottom)
|
||||
.offset(-22.),
|
||||
]);
|
||||
widgets::auto_layout(
|
||||
&view,
|
||||
vec![
|
||||
&self.show_bootrom.view,
|
||||
&self.prefer_cgb.view,
|
||||
&self.dmg_bootrom.view,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct StandalonePreferencesContentView {}
|
||||
pub(crate) struct StandalonePreferencesContentView {
|
||||
config: StandaloneConfig,
|
||||
config_manager: ConfigManager,
|
||||
group_screenshots_by_rom: ToggleView,
|
||||
}
|
||||
|
||||
impl StandalonePreferencesContentView {
|
||||
pub(crate) fn new(config_manager: ConfigManager) -> Self {
|
||||
Self {
|
||||
config: config_manager.load_or_create_config(),
|
||||
config_manager,
|
||||
group_screenshots_by_rom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: StandalonePreferencesUpdates) {
|
||||
match update {
|
||||
StandalonePreferencesUpdates::GroupScreenshotsByRom => {
|
||||
self.config.group_screenshots_by_rom = !self.config.group_screenshots_by_rom
|
||||
}
|
||||
}
|
||||
self.config_manager
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum StandalonePreferencesUpdates {
|
||||
GroupScreenshotsByRom,
|
||||
}
|
||||
|
||||
impl ViewDelegate for StandalonePreferencesContentView {
|
||||
const NAME: &'static str = "StandalonePreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.group_screenshots_by_rom.configure(
|
||||
"Group screenshots by ROM",
|
||||
"",
|
||||
self.config.group_screenshots_by_rom,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateStandalone(
|
||||
StandalonePreferencesUpdates::GroupScreenshotsByRom,
|
||||
),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
view.add_subview(&self.group_screenshots_by_rom.view);
|
||||
|
||||
widgets::auto_layout(&view, vec![&self.group_screenshots_by_rom.view]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct VstPreferencesContentView {}
|
||||
pub(crate) struct VstPreferencesContentView {
|
||||
config: twinc_emu_vst::VstConfig,
|
||||
config_manager: ConfigManager,
|
||||
force_skip_bootrom: ToggleView,
|
||||
}
|
||||
|
||||
impl VstPreferencesContentView {
|
||||
pub(crate) fn new(config_manager: ConfigManager) -> Self {
|
||||
Self {
|
||||
config: config_manager.load_or_create_config(),
|
||||
config_manager,
|
||||
force_skip_bootrom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn update(&mut self, update: VstPreferencesUpdates) {
|
||||
match update {
|
||||
VstPreferencesUpdates::ForceSkipBootrom => {
|
||||
self.config.force_skip_bootrom = !self.config.force_skip_bootrom
|
||||
}
|
||||
}
|
||||
self.config_manager
|
||||
.save_custom_config(self.config.clone())
|
||||
.expect("failed to save config");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum VstPreferencesUpdates {
|
||||
ForceSkipBootrom,
|
||||
}
|
||||
|
||||
impl ViewDelegate for VstPreferencesContentView {
|
||||
const NAME: &'static str = "VstPreferencesContentView";
|
||||
|
||||
fn did_load(&mut self, view: View) {
|
||||
self.force_skip_bootrom.configure(
|
||||
"Force skip bootrom",
|
||||
"",
|
||||
self.config.force_skip_bootrom,
|
||||
|_v| {
|
||||
dispatch(crate::macos::AppMessage::Preferences(
|
||||
super::PreferencesMessage::UpdateVst(VstPreferencesUpdates::ForceSkipBootrom),
|
||||
))
|
||||
},
|
||||
);
|
||||
|
||||
view.add_subview(&self.force_skip_bootrom.view);
|
||||
|
||||
widgets::auto_layout(&view, vec![&self.force_skip_bootrom.view]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,44 @@
|
|||
use cacao::button::Button;
|
||||
use cacao::input::TextField;
|
||||
use cacao::layout::{Layout, LayoutConstraint};
|
||||
use cacao::switch::Switch;
|
||||
use cacao::text::Label;
|
||||
use cacao::view::View;
|
||||
use objc::runtime::Object;
|
||||
|
||||
pub(crate) fn auto_layout(container: &View, items: Vec<&View>) {
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
LayoutConstraint::activate(&[
|
||||
if i > 0 {
|
||||
item.top.constraint_equal_to(&items[i - 1].bottom)
|
||||
} else {
|
||||
item.top.constraint_equal_to(&container.top).offset(22.)
|
||||
},
|
||||
item.leading
|
||||
.constraint_equal_to(&container.leading)
|
||||
.offset(22.),
|
||||
item.trailing
|
||||
.constraint_equal_to(&container.trailing)
|
||||
.offset(-22.),
|
||||
]);
|
||||
}
|
||||
LayoutConstraint::activate(&[items
|
||||
.last()
|
||||
.unwrap()
|
||||
.bottom
|
||||
.constraint_equal_to(&container.bottom)
|
||||
.offset(-22.)]);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToggleOptionView {
|
||||
pub struct ToggleView {
|
||||
pub view: View,
|
||||
pub switch: Switch,
|
||||
pub title: Label,
|
||||
pub subtitle: Label,
|
||||
}
|
||||
|
||||
impl Default for ToggleOptionView {
|
||||
impl Default for ToggleView {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
|
||||
|
@ -41,7 +67,7 @@ impl Default for ToggleOptionView {
|
|||
.constraint_greater_than_or_equal_to_constant(200.),
|
||||
]);
|
||||
|
||||
ToggleOptionView {
|
||||
ToggleView {
|
||||
view,
|
||||
switch,
|
||||
title,
|
||||
|
@ -50,7 +76,7 @@ impl Default for ToggleOptionView {
|
|||
}
|
||||
}
|
||||
|
||||
impl ToggleOptionView {
|
||||
impl ToggleView {
|
||||
pub fn configure<F>(&mut self, text: &str, subtitle: &str, state: bool, handler: F)
|
||||
where
|
||||
F: Fn(*const Object) + Send + Sync + 'static,
|
||||
|
@ -61,3 +87,52 @@ impl ToggleOptionView {
|
|||
self.switch.set_checked(state);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathView {
|
||||
pub view: View,
|
||||
pub field: TextField,
|
||||
pub button: Button,
|
||||
}
|
||||
|
||||
impl Default for PathView {
|
||||
fn default() -> Self {
|
||||
let view = View::new();
|
||||
let field = TextField::new();
|
||||
field.set_uses_single_line(true);
|
||||
view.add_subview(&field);
|
||||
let button = Button::new("Browse");
|
||||
view.add_subview(&button);
|
||||
|
||||
LayoutConstraint::activate(&[
|
||||
field.top.constraint_equal_to(&view.top),
|
||||
field.leading.constraint_equal_to(&view.leading),
|
||||
field.bottom.constraint_equal_to(&view.bottom),
|
||||
field
|
||||
.trailing
|
||||
.constraint_equal_to(&button.leading)
|
||||
.offset(-10.),
|
||||
field
|
||||
.width
|
||||
.constraint_greater_than_or_equal_to_constant(400.),
|
||||
button.trailing.constraint_equal_to(&view.trailing),
|
||||
button.width.constraint_equal_to_constant(100.),
|
||||
button.top.constraint_equal_to(&view.top),
|
||||
button.bottom.constraint_equal_to(&view.bottom),
|
||||
]);
|
||||
|
||||
Self {
|
||||
view,
|
||||
field,
|
||||
button,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PathView {
|
||||
pub fn configure(&mut self, placeholder: &str, state: Option<String>) {
|
||||
self.field.set_placeholder_text(placeholder);
|
||||
if let Some(state) = state {
|
||||
self.field.set_text(&state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@ compile_error!("select one rendering backend!");
|
|||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct StandaloneConfig {
|
||||
scale_factor: usize,
|
||||
group_screenshots_by_rom: bool,
|
||||
buffers_per_frame: usize,
|
||||
output_buffer_size: u32,
|
||||
pub scale_factor: usize,
|
||||
pub group_screenshots_by_rom: bool,
|
||||
pub buffers_per_frame: usize,
|
||||
pub output_buffer_size: u32,
|
||||
}
|
||||
|
||||
impl NamedConfig for StandaloneConfig {
|
||||
|
|
|
@ -7,17 +7,24 @@ edition = "2021"
|
|||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[features]
|
||||
default = ["vulkan-static"]
|
||||
default = ["plugin", "vulkan-static"]
|
||||
pixels = ["gb-emu-lib/pixels-renderer"]
|
||||
vulkan = ["dep:raw-window-handle", "gb-emu-lib/vulkan-renderer"]
|
||||
vulkan-static = ["vulkan", "gb-emu-lib/vulkan-static"]
|
||||
plugin = [
|
||||
"dep:nih_plug",
|
||||
"dep:baseview",
|
||||
"dep:async-ringbuf",
|
||||
"dep:futures",
|
||||
"dep:keyboard-types",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
gb-emu-lib = { workspace = true }
|
||||
nih_plug = { workspace = true, features = ["standalone"] }
|
||||
baseview = { workspace = true }
|
||||
async-ringbuf = "0.1"
|
||||
futures = "0.3"
|
||||
keyboard-types = "0.6.2"
|
||||
nih_plug = { workspace = true, features = ["standalone"], optional = true }
|
||||
baseview = { workspace = true, optional = true }
|
||||
async-ringbuf = { version = "0.1", optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
keyboard-types = { version = "0.6.2", optional = true }
|
||||
raw-window-handle = { version = "0.5", optional = true }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
|
@ -1,72 +1,15 @@
|
|||
use async_ringbuf::AsyncHeapConsumer;
|
||||
use baseview::Size;
|
||||
use futures::executor;
|
||||
use gb_emu_lib::{
|
||||
config::{ConfigManager, NamedConfig},
|
||||
connect::{
|
||||
AudioOutput, CgbRomType, DownsampleType, EmulatorCoreTrait, EmulatorMessage,
|
||||
EmulatorOptions, NoCamera, RendererMessage, RomFile, SerialTarget,
|
||||
},
|
||||
EmulatorCore, HEIGHT, WIDTH,
|
||||
};
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField};
|
||||
|
||||
use gb_emu_lib::config::NamedConfig;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
Arc, Mutex, OnceLock, RwLock,
|
||||
},
|
||||
};
|
||||
use ui::TwincEditor;
|
||||
|
||||
mod ui;
|
||||
|
||||
#[derive(Default)]
|
||||
struct SramParam {
|
||||
state: Arc<RwLock<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl PersistentField<'_, Vec<u8>> for SramParam {
|
||||
fn set(&self, new_value: Vec<u8>) {
|
||||
let mut w = self.state.write().unwrap();
|
||||
w.resize(new_value.len(), 0);
|
||||
w.copy_from_slice(&new_value);
|
||||
}
|
||||
|
||||
fn map<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: Fn(&Vec<u8>) -> R,
|
||||
{
|
||||
f(&self.state.read().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Default)]
|
||||
struct EmuParams {
|
||||
#[persist = "sram"]
|
||||
sram_save: SramParam,
|
||||
}
|
||||
|
||||
struct EmuVars {
|
||||
rx: AsyncHeapConsumer<[f32; 2]>,
|
||||
emulator_core: EmulatorCore<[u8; 4], NoCamera>,
|
||||
serial_tx: Sender<u8>,
|
||||
}
|
||||
|
||||
struct EmuComms {
|
||||
sender: Sender<EmulatorMessage>,
|
||||
receiver: Receiver<RendererMessage<[u8; 4]>>,
|
||||
}
|
||||
#[cfg(feature = "plugin")]
|
||||
mod plugin;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[serde(default)]
|
||||
pub struct VstConfig {
|
||||
scale_factor: usize,
|
||||
rom: String,
|
||||
force_skip_bootrom: bool,
|
||||
pub scale_factor: usize,
|
||||
pub rom: String,
|
||||
pub force_skip_bootrom: bool,
|
||||
}
|
||||
|
||||
impl NamedConfig for VstConfig {
|
||||
|
@ -84,302 +27,3 @@ impl Default for VstConfig {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Configs {
|
||||
vst_config: VstConfig,
|
||||
emu_config: gb_emu_lib::config::Config,
|
||||
config_dir: PathBuf,
|
||||
}
|
||||
|
||||
static CONFIGS: OnceLock<Configs> = OnceLock::new();
|
||||
static IS_CGB: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
fn access_config<'a>() -> &'a Configs {
|
||||
CONFIGS.get_or_init(|| {
|
||||
let config_manager = ConfigManager::get().expect("Could not open config folder");
|
||||
let emu_config = config_manager.load_or_create_base_config();
|
||||
let vst_config: VstConfig = config_manager.load_or_create_config();
|
||||
|
||||
Configs {
|
||||
vst_config,
|
||||
emu_config,
|
||||
config_dir: config_manager.dir(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GameboyEmu {
|
||||
vars: Option<EmuVars>,
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
params: Arc<EmuParams>,
|
||||
}
|
||||
|
||||
const BUFFERS_PER_FRAME: usize = 1;
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::Linear;
|
||||
|
||||
impl Plugin for GameboyEmu {
|
||||
const NAME: &'static str = "Gameboy";
|
||||
|
||||
const VENDOR: &'static str = "Alex Janka";
|
||||
|
||||
const URL: &'static str = "alexjanka.com";
|
||||
|
||||
const EMAIL: &'static str = "alex@alexjanka.com";
|
||||
|
||||
const VERSION: &'static str = "0.1";
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(2),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(1),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
];
|
||||
|
||||
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
||||
|
||||
type SysExMessage = ();
|
||||
|
||||
type BackgroundTask = ();
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
while let Some(event) = context.next_event() {
|
||||
if let Some(Basic(as_bytes)) = event.as_midi() {
|
||||
match event {
|
||||
NoteEvent::NoteOn {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x90 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::NoteOff {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x80 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiPitchBend {
|
||||
timing: _,
|
||||
channel,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xE0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiCC {
|
||||
timing: _,
|
||||
channel,
|
||||
cc: _,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xB0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiProgramChange {
|
||||
timing: _,
|
||||
channel,
|
||||
program: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xC0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if buffer.channels() != 2 {
|
||||
for mut sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
if let Some(g) = sample.get_mut(0) {
|
||||
*g = (a[0] + a[1]) / 2.;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
for (source, dest) in a.iter().zip(sample) {
|
||||
*dest = *source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
} else {
|
||||
while context.next_event().is_some() {}
|
||||
}
|
||||
ProcessStatus::KeepAlive
|
||||
}
|
||||
|
||||
fn editor(&mut self, _e: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
let configs = access_config();
|
||||
|
||||
let size = Size::new(
|
||||
(WIDTH * configs.vst_config.scale_factor) as f64,
|
||||
(HEIGHT * configs.vst_config.scale_factor) as f64,
|
||||
);
|
||||
|
||||
Some(Box::new(TwincEditor::new(self.emu_comms.clone(), size)))
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
vars.emulator_core.replace_output(output);
|
||||
vars.rx = rx;
|
||||
} else {
|
||||
let configs = access_config();
|
||||
let rom_path = configs.config_dir.join(configs.vst_config.rom.clone());
|
||||
|
||||
let (rom, camera) = RomFile::Path(rom_path)
|
||||
.load(gb_emu_lib::connect::SramType::None, NoCamera::default())
|
||||
.unwrap_or_else(|_v| {
|
||||
RomFile::Raw(include_bytes!("../error.gb").to_vec())
|
||||
.load(gb_emu_lib::connect::SramType::None, NoCamera::default())
|
||||
.expect("Couldn't load built-in fallback rom")
|
||||
});
|
||||
|
||||
let _ =
|
||||
IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb);
|
||||
|
||||
let (sender, receiver) = channel::<EmulatorMessage>();
|
||||
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
let (emu_sender, renderer_receiver) = mpsc::channel();
|
||||
|
||||
*self.emu_comms.lock().unwrap() = Some(EmuComms {
|
||||
sender,
|
||||
receiver: renderer_receiver,
|
||||
});
|
||||
|
||||
let (serial_tx, gb_serial_rx) = mpsc::channel::<u8>();
|
||||
let serial_target = SerialTarget::Custom {
|
||||
rx: Some(gb_serial_rx),
|
||||
tx: None,
|
||||
};
|
||||
|
||||
let will_skip_bootrom =
|
||||
configs.vst_config.force_skip_bootrom || !configs.emu_config.show_bootrom;
|
||||
|
||||
let mut emulator_core = {
|
||||
let options = EmulatorOptions::new_with_config(
|
||||
configs.emu_config.clone(),
|
||||
configs.config_dir.clone(),
|
||||
emu_sender,
|
||||
rom,
|
||||
output,
|
||||
)
|
||||
.with_serial_target(serial_target)
|
||||
.with_sram_buffer(self.params.sram_save.state.clone())
|
||||
.with_show_bootrom(!will_skip_bootrom);
|
||||
|
||||
EmulatorCore::init(false, receiver, options, camera)
|
||||
};
|
||||
|
||||
emulator_core.run_until_buffer_full();
|
||||
|
||||
self.vars = Some(EmuVars {
|
||||
rx,
|
||||
emulator_core,
|
||||
serial_tx,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn deactivate(&mut self) {
|
||||
if let Ok(comms) = self.emu_comms.lock() {
|
||||
if let Some(ref comms) = *comms {
|
||||
match comms.sender.send(EmulatorMessage::Exit) {
|
||||
Ok(_) => self.vars = None,
|
||||
Err(e) => nih_log!("error {e} sending message to emulator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vst3Plugin for GameboyEmu {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula";
|
||||
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
||||
&[Vst3SubCategory::Instrument, Vst3SubCategory::Synth];
|
||||
}
|
||||
|
||||
nih_export_vst3!(GameboyEmu);
|
||||
|
|
361
gb-vst/src/plugin.rs
Normal file
361
gb-vst/src/plugin.rs
Normal file
|
@ -0,0 +1,361 @@
|
|||
use async_ringbuf::AsyncHeapConsumer;
|
||||
use baseview::Size;
|
||||
use futures::executor;
|
||||
use gb_emu_lib::{
|
||||
config::ConfigManager,
|
||||
connect::{
|
||||
AudioOutput, CgbRomType, DownsampleType, EmulatorCoreTrait, EmulatorMessage,
|
||||
EmulatorOptions, NoCamera, RendererMessage, RomFile, SerialTarget,
|
||||
},
|
||||
EmulatorCore, HEIGHT, WIDTH,
|
||||
};
|
||||
use nih_plug::prelude::*;
|
||||
use nih_plug::{midi::MidiResult::Basic, params::persist::PersistentField};
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
mpsc::{self, channel, Receiver, Sender},
|
||||
Arc, Mutex, OnceLock, RwLock,
|
||||
},
|
||||
};
|
||||
use ui::TwincEditor;
|
||||
|
||||
use crate::VstConfig;
|
||||
|
||||
mod ui;
|
||||
|
||||
#[derive(Default)]
|
||||
struct SramParam {
|
||||
state: Arc<RwLock<Vec<u8>>>,
|
||||
}
|
||||
|
||||
impl PersistentField<'_, Vec<u8>> for SramParam {
|
||||
fn set(&self, new_value: Vec<u8>) {
|
||||
let mut w = self.state.write().unwrap();
|
||||
w.resize(new_value.len(), 0);
|
||||
w.copy_from_slice(&new_value);
|
||||
}
|
||||
|
||||
fn map<F, R>(&self, f: F) -> R
|
||||
where
|
||||
F: Fn(&Vec<u8>) -> R,
|
||||
{
|
||||
f(&self.state.read().unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Params, Default)]
|
||||
struct EmuParams {
|
||||
#[persist = "sram"]
|
||||
sram_save: SramParam,
|
||||
}
|
||||
|
||||
struct EmuVars {
|
||||
rx: AsyncHeapConsumer<[f32; 2]>,
|
||||
emulator_core: EmulatorCore<[u8; 4], NoCamera>,
|
||||
serial_tx: Sender<u8>,
|
||||
}
|
||||
|
||||
struct EmuComms {
|
||||
sender: Sender<EmulatorMessage>,
|
||||
receiver: Receiver<RendererMessage<[u8; 4]>>,
|
||||
}
|
||||
|
||||
struct Configs {
|
||||
vst_config: VstConfig,
|
||||
emu_config: gb_emu_lib::config::Config,
|
||||
config_dir: PathBuf,
|
||||
}
|
||||
|
||||
static CONFIGS: OnceLock<Configs> = OnceLock::new();
|
||||
static IS_CGB: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
fn access_config<'a>() -> &'a Configs {
|
||||
CONFIGS.get_or_init(|| {
|
||||
let config_manager = ConfigManager::get().expect("Could not open config folder");
|
||||
let emu_config = config_manager.load_or_create_base_config();
|
||||
let vst_config: VstConfig = config_manager.load_or_create_config();
|
||||
|
||||
Configs {
|
||||
vst_config,
|
||||
emu_config,
|
||||
config_dir: config_manager.dir(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct GameboyEmu {
|
||||
vars: Option<EmuVars>,
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
params: Arc<EmuParams>,
|
||||
}
|
||||
|
||||
const BUFFERS_PER_FRAME: usize = 1;
|
||||
const DOWNSAMPLE_TYPE: DownsampleType = DownsampleType::Linear;
|
||||
|
||||
impl Plugin for GameboyEmu {
|
||||
const NAME: &'static str = "Gameboy";
|
||||
|
||||
const VENDOR: &'static str = "Alex Janka";
|
||||
|
||||
const URL: &'static str = "alexjanka.com";
|
||||
|
||||
const EMAIL: &'static str = "alex@alexjanka.com";
|
||||
|
||||
const VERSION: &'static str = "0.1";
|
||||
|
||||
const AUDIO_IO_LAYOUTS: &'static [AudioIOLayout] = &[
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(2),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
AudioIOLayout {
|
||||
main_input_channels: None,
|
||||
main_output_channels: NonZeroU32::new(1),
|
||||
|
||||
aux_input_ports: &[],
|
||||
aux_output_ports: &[],
|
||||
|
||||
// Individual ports and the layout as a whole can be named here. By default these names
|
||||
// are generated as needed. This layout will be called 'Stereo', while the other one is
|
||||
// given the name 'Mono' based no the number of input and output channels.
|
||||
names: PortNames::const_default(),
|
||||
},
|
||||
];
|
||||
|
||||
const MIDI_INPUT: MidiConfig = MidiConfig::MidiCCs;
|
||||
const SAMPLE_ACCURATE_AUTOMATION: bool = true;
|
||||
|
||||
type SysExMessage = ();
|
||||
|
||||
type BackgroundTask = ();
|
||||
|
||||
fn params(&self) -> Arc<dyn Params> {
|
||||
self.params.clone()
|
||||
}
|
||||
|
||||
fn process(
|
||||
&mut self,
|
||||
buffer: &mut Buffer,
|
||||
_: &mut AuxiliaryBuffers,
|
||||
context: &mut impl ProcessContext<Self>,
|
||||
) -> ProcessStatus {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
while let Some(event) = context.next_event() {
|
||||
if let Some(Basic(as_bytes)) = event.as_midi() {
|
||||
match event {
|
||||
NoteEvent::NoteOn {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x90 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::NoteOff {
|
||||
timing: _,
|
||||
voice_id: _,
|
||||
channel,
|
||||
note: _,
|
||||
velocity: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0x80 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiPitchBend {
|
||||
timing: _,
|
||||
channel,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xE0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiCC {
|
||||
timing: _,
|
||||
channel,
|
||||
cc: _,
|
||||
value: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xB0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
NoteEvent::MidiProgramChange {
|
||||
timing: _,
|
||||
channel,
|
||||
program: _,
|
||||
} => {
|
||||
if channel < 5 {
|
||||
vars.serial_tx.send(0xC0 + channel).unwrap();
|
||||
vars.serial_tx.send(as_bytes[1]).unwrap();
|
||||
vars.serial_tx.send(as_bytes[2]).unwrap();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
if buffer.channels() != 2 {
|
||||
for mut sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
if let Some(g) = sample.get_mut(0) {
|
||||
*g = (a[0] + a[1]) / 2.;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for sample in buffer.iter_samples() {
|
||||
if vars.rx.is_empty() {
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
}
|
||||
if let Some(a) = executor::block_on(vars.rx.pop()) {
|
||||
for (source, dest) in a.iter().zip(sample) {
|
||||
*dest = *source;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
vars.emulator_core.run_until_buffer_full();
|
||||
} else {
|
||||
while context.next_event().is_some() {}
|
||||
}
|
||||
ProcessStatus::KeepAlive
|
||||
}
|
||||
|
||||
fn editor(&mut self, _e: AsyncExecutor<Self>) -> Option<Box<dyn Editor>> {
|
||||
let configs = access_config();
|
||||
|
||||
let size = Size::new(
|
||||
(WIDTH * configs.vst_config.scale_factor) as f64,
|
||||
(HEIGHT * configs.vst_config.scale_factor) as f64,
|
||||
);
|
||||
|
||||
Some(Box::new(TwincEditor::new(self.emu_comms.clone(), size)))
|
||||
}
|
||||
|
||||
fn initialize(
|
||||
&mut self,
|
||||
_audio_io_layout: &AudioIOLayout,
|
||||
buffer_config: &BufferConfig,
|
||||
_context: &mut impl InitContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(ref mut vars) = self.vars {
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
vars.emulator_core.replace_output(output);
|
||||
vars.rx = rx;
|
||||
} else {
|
||||
let configs = access_config();
|
||||
let rom_path = configs.config_dir.join(configs.vst_config.rom.clone());
|
||||
|
||||
let (rom, camera) = RomFile::Path(rom_path)
|
||||
.load(gb_emu_lib::connect::SramType::None, NoCamera::default())
|
||||
.unwrap_or_else(|_v| {
|
||||
RomFile::Raw(include_bytes!("../error.gb").to_vec())
|
||||
.load(gb_emu_lib::connect::SramType::None, NoCamera::default())
|
||||
.expect("Couldn't load built-in fallback rom")
|
||||
});
|
||||
|
||||
let _ =
|
||||
IS_CGB.set(rom.rom_type == CgbRomType::CgbOnly || configs.emu_config.prefer_cgb);
|
||||
|
||||
let (sender, receiver) = channel::<EmulatorMessage>();
|
||||
|
||||
let (output, rx) = AudioOutput::new(
|
||||
buffer_config.sample_rate,
|
||||
BUFFERS_PER_FRAME,
|
||||
DOWNSAMPLE_TYPE,
|
||||
);
|
||||
|
||||
let (emu_sender, renderer_receiver) = mpsc::channel();
|
||||
|
||||
*self.emu_comms.lock().unwrap() = Some(EmuComms {
|
||||
sender,
|
||||
receiver: renderer_receiver,
|
||||
});
|
||||
|
||||
let (serial_tx, gb_serial_rx) = mpsc::channel::<u8>();
|
||||
let serial_target = SerialTarget::Custom {
|
||||
rx: Some(gb_serial_rx),
|
||||
tx: None,
|
||||
};
|
||||
|
||||
let will_skip_bootrom =
|
||||
configs.vst_config.force_skip_bootrom || !configs.emu_config.show_bootrom;
|
||||
|
||||
let mut emulator_core = {
|
||||
let options = EmulatorOptions::new_with_config(
|
||||
configs.emu_config.clone(),
|
||||
configs.config_dir.clone(),
|
||||
emu_sender,
|
||||
rom,
|
||||
output,
|
||||
)
|
||||
.with_serial_target(serial_target)
|
||||
.with_sram_buffer(self.params.sram_save.state.clone())
|
||||
.with_show_bootrom(!will_skip_bootrom);
|
||||
|
||||
EmulatorCore::init(false, receiver, options, camera)
|
||||
};
|
||||
|
||||
emulator_core.run_until_buffer_full();
|
||||
|
||||
self.vars = Some(EmuVars {
|
||||
rx,
|
||||
emulator_core,
|
||||
serial_tx,
|
||||
});
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn deactivate(&mut self) {
|
||||
if let Ok(comms) = self.emu_comms.lock() {
|
||||
if let Some(ref comms) = *comms {
|
||||
match comms.sender.send(EmulatorMessage::Exit) {
|
||||
Ok(_) => self.vars = None,
|
||||
Err(e) => nih_log!("error {e} sending message to emulator"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Vst3Plugin for GameboyEmu {
|
||||
const VST3_CLASS_ID: [u8; 16] = *b"alexjankagbemula";
|
||||
|
||||
const VST3_SUBCATEGORIES: &'static [Vst3SubCategory] =
|
||||
&[Vst3SubCategory::Instrument, Vst3SubCategory::Synth];
|
||||
}
|
||||
|
||||
nih_export_vst3!(GameboyEmu);
|
|
@ -10,7 +10,7 @@ use gb_emu_lib::{
|
|||
use keyboard_types::{Code, KeyState};
|
||||
use nih_plug::prelude::*;
|
||||
|
||||
use crate::{access_config, EmuComms};
|
||||
use super::{access_config, EmuComms};
|
||||
|
||||
pub struct TwincEditor {
|
||||
emu_comms: Arc<Mutex<Option<EmuComms>>>,
|
||||
|
@ -47,7 +47,7 @@ impl Editor for TwincEditor {
|
|||
|
||||
#[cfg(feature = "vulkan")]
|
||||
let shader_path = {
|
||||
if crate::IS_CGB.get().is_some_and(|v| *v) {
|
||||
if super::IS_CGB.get().is_some_and(|v| *v) {
|
||||
config.emu_config.vulkan_config.cgb_shader_path.as_ref()
|
||||
} else {
|
||||
config.emu_config.vulkan_config.dmg_shader_path.as_ref()
|
|
@ -10,6 +10,7 @@ pub trait NamedConfig {
|
|||
fn name() -> String;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ConfigManager {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue