diff --git a/Cargo.lock b/Cargo.lock
index ce66d3c..e499139 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/gb-emu/Cargo.toml b/gb-emu/Cargo.toml
index 486691f..ce5d7ec 100644
--- a/gb-emu/Cargo.toml
+++ b/gb-emu/Cargo.toml
@@ -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" }
diff --git a/gb-emu/src/audio.rs b/gb-emu/src/audio.rs
index 98b42e5..50b15da 100644
--- a/gb-emu/src/audio.rs
+++ b/gb-emu/src/audio.rs
@@ -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);
                         }
                     }
                 },
diff --git a/gb-emu/src/bin/cli.rs b/gb-emu/src/bin/cli.rs
index d2c0834..8b79814 100644
--- a/gb-emu/src/bin/cli.rs
+++ b/gb-emu/src/bin/cli.rs
@@ -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);
diff --git a/gb-emu/src/bin/macos/cacao_window_manager.rs b/gb-emu/src/bin/macos/cacao_window_manager.rs
index 58a5664..482691c 100644
--- a/gb-emu/src/bin/macos/cacao_window_manager.rs
+++ b/gb-emu/src/bin/macos/cacao_window_manager.rs
@@ -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)
     }
 }
diff --git a/gb-emu/src/bin/macos/mod.rs b/gb-emu/src/bin/macos/mod.rs
index 7b851dc..d9cea9f 100644
--- a/gb-emu/src/bin/macos/mod.rs
+++ b/gb-emu/src/bin/macos/mod.rs
@@ -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",
diff --git a/gb-emu/src/lib.rs b/gb-emu/src/lib.rs
index 45c7e33..f04e592 100644
--- a/gb-emu/src/lib.rs
+++ b/gb-emu/src/lib.rs
@@ -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);
diff --git a/gb-emu/src/window.rs b/gb-emu/src/window.rs
index f4cd22a..53458d6 100644
--- a/gb-emu/src/window.rs
+++ b/gb-emu/src/window.rs
@@ -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>;
 }
diff --git a/gb-emu/src/window/winit_manager.rs b/gb-emu/src/window/winit_manager.rs
index 57a70df..f18179d 100644
--- a/gb-emu/src/window/winit_manager.rs
+++ b/gb-emu/src/window/winit_manager.rs
@@ -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 {
diff --git a/lib/src/connect/mod.rs b/lib/src/connect/mod.rs
index 428ebdb..4c5ae54 100644
--- a/lib/src/connect/mod.rs
+++ b/lib/src/connect/mod.rs
@@ -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;
 }
diff --git a/lib/src/lib.rs b/lib/src/lib.rs
index fd1a082..3569471 100644
--- a/lib/src/lib.rs
+++ b/lib/src/lib.rs
@@ -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()
     }
 }