use std::cell::RefCell; use std::path::{Path, PathBuf}; use std::rc::Rc; use std::time::Duration; use gtk::gio; use gtk::gdk; use gtk::glib; use gtk::prelude::*; use gtk4 as gtk; use nesemu::prelude::{EmulationState, HostConfig, RuntimeHostLoop}; use nesemu::{ FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH, FrameClock, InputProvider, JoypadButton, JoypadButtons, NesRuntime, set_button_pressed, }; const APP_ID: &str = "org.nesemu.desktop"; const TITLE: &str = "NES Emulator"; const SCALE: i32 = 3; const SAMPLE_RATE: u32 = 48_000; fn main() { if std::env::var_os("GSK_RENDERER").is_none() { unsafe { std::env::set_var("GSK_RENDERER", "cairo"); } } let app = gtk::Application::builder() .application_id(APP_ID) .build(); let initial_rom: Rc>> = Rc::new(RefCell::new(std::env::args().nth(1).map(PathBuf::from))); let initial_rom_for_activate = Rc::clone(&initial_rom); app.connect_activate(move |app| { let rom = initial_rom_for_activate.borrow_mut().take(); build_ui(app, rom); }); app.run_with_args::<&str>(&[]); } fn build_ui(app: >k::Application, initial_rom: Option) { let window = gtk::ApplicationWindow::builder() .application(app) .title(TITLE) .default_width((FRAME_WIDTH as i32) * SCALE) .default_height((FRAME_HEIGHT as i32) * SCALE) .build(); // --- Header bar --- let header = gtk::HeaderBar::new(); let open_button = gtk::Button::builder() .icon_name("document-open-symbolic") .tooltip_text("Open ROM (Ctrl+O)") .focusable(false) .build(); let pause_button = gtk::Button::builder() .icon_name("media-playback-pause-symbolic") .tooltip_text("Pause / Resume (P)") .focusable(false) .sensitive(false) .build(); let reset_button = gtk::Button::builder() .icon_name("view-refresh-symbolic") .tooltip_text("Reset (Ctrl+R)") .focusable(false) .sensitive(false) .build(); header.pack_start(&open_button); header.pack_start(&pause_button); header.pack_start(&reset_button); window.set_titlebar(Some(&header)); // --- Drawing area --- let drawing_area = gtk::DrawingArea::new(); drawing_area.set_hexpand(true); drawing_area.set_vexpand(true); let overlay = gtk::Overlay::new(); overlay.set_child(Some(&drawing_area)); let drop_label = gtk::Label::builder() .label("Drop a .nes ROM here\nor press Ctrl+O to open") .justify(gtk::Justification::Center) .css_classes(["dim-label"]) .build(); drop_label.set_halign(gtk::Align::Center); drop_label.set_valign(gtk::Align::Center); overlay.add_overlay(&drop_label); window.set_child(Some(&overlay)); // --- State --- let desktop = Rc::new(RefCell::new(DesktopApp::new())); let frame_for_draw: Rc>> = Rc::new(RefCell::new(vec![0u8; FRAME_RGBA_BYTES])); // --- Draw function (pixel-perfect nearest-neighbor) --- { let frame_for_draw = Rc::clone(&frame_for_draw); drawing_area.set_draw_func(move |_da, cr, width, height| { let frame = frame_for_draw.borrow(); let stride = cairo::Format::ARgb32.stride_for_width(FRAME_WIDTH as u32).unwrap(); let mut argb = vec![0u8; stride as usize * FRAME_HEIGHT]; for y in 0..FRAME_HEIGHT { for x in 0..FRAME_WIDTH { let src = (y * FRAME_WIDTH + x) * 4; let dst = y * stride as usize + x * 4; let r = frame[src]; let g = frame[src + 1]; let b = frame[src + 2]; let a = frame[src + 3]; argb[dst] = b; argb[dst + 1] = g; argb[dst + 2] = r; argb[dst + 3] = a; } } let surface = cairo::ImageSurface::create_for_data( argb, cairo::Format::ARgb32, FRAME_WIDTH as i32, FRAME_HEIGHT as i32, stride, ) .expect("Failed to create Cairo surface"); // Fill background black let _ = cr.set_source_rgb(0.0, 0.0, 0.0); let _ = cr.paint(); let sx = width as f64 / FRAME_WIDTH as f64; let sy = height as f64 / FRAME_HEIGHT as f64; let scale = sx.min(sy); let offset_x = (width as f64 - FRAME_WIDTH as f64 * scale) / 2.0; let offset_y = (height as f64 - FRAME_HEIGHT as f64 * scale) / 2.0; let _ = cr.translate(offset_x, offset_y); let _ = cr.scale(scale, scale); let _ = cr.set_source_surface(&surface, 0.0, 0.0); cr.source().set_filter(cairo::Filter::Nearest); let _ = cr.paint(); }); } // --- Helper to sync UI with emulation state --- let sync_ui = { let pause_button = pause_button.clone(); let reset_button = reset_button.clone(); let drop_label = drop_label.clone(); let window = window.clone(); move |app_state: &DesktopApp, rom_name: Option<&str>| { let loaded = app_state.is_loaded(); pause_button.set_sensitive(loaded); reset_button.set_sensitive(loaded); drop_label.set_visible(!loaded); if app_state.state() == EmulationState::Running { pause_button.set_icon_name("media-playback-pause-symbolic"); pause_button.set_tooltip_text(Some("Pause (P)")); } else { pause_button.set_icon_name("media-playback-start-symbolic"); pause_button.set_tooltip_text(Some("Resume (P)")); } if let Some(name) = rom_name { window.set_title(Some(&format!("{TITLE} — {name}"))); } } }; let sync_ui = Rc::new(sync_ui); // --- Load initial ROM --- { let mut app_state = desktop.borrow_mut(); if let Some(path) = initial_rom { if let Err(err) = app_state.load_rom_from_path(&path) { eprintln!("Failed to load ROM '{}': {err}", path.display()); sync_ui(&app_state, None); } else { let name = rom_filename(&path); sync_ui(&app_state, Some(&name)); } } else { sync_ui(&app_state, None); } } // --- Open ROM handler --- let do_open_rom = { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); let window = window.clone(); Rc::new(move || { let chooser = gtk::FileChooserNative::new( Some("Open NES ROM"), Some(&window), gtk::FileChooserAction::Open, Some("Open"), Some("Cancel"), ); let nes_filter = gtk::FileFilter::new(); nes_filter.set_name(Some("NES ROMs")); nes_filter.add_pattern("*.nes"); chooser.add_filter(&nes_filter); let all_filter = gtk::FileFilter::new(); all_filter.set_name(Some("All files")); all_filter.add_pattern("*"); chooser.add_filter(&all_filter); let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); chooser.connect_response(move |dialog, response| { if response == gtk::ResponseType::Accept { if let Some(path) = dialog.file().and_then(|f| f.path()) { let mut app_state = desktop.borrow_mut(); if let Err(err) = app_state.load_rom_from_path(&path) { eprintln!("Failed to load ROM '{}': {err}", path.display()); } else { let name = rom_filename(&path); sync_ui(&app_state, Some(&name)); } } } }); chooser.show(); }) }; // --- Button handlers --- { let do_open_rom = Rc::clone(&do_open_rom); open_button.connect_clicked(move |_| { do_open_rom(); }); } { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); pause_button.connect_clicked(move |_| { let mut app_state = desktop.borrow_mut(); app_state.toggle_pause(); sync_ui(&app_state, None); }); } { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); reset_button.connect_clicked(move |_| { let mut app_state = desktop.borrow_mut(); app_state.reset(); sync_ui(&app_state, None); }); } // --- Keyboard shortcuts via actions --- let action_open = gio::SimpleAction::new("open", None); { let do_open_rom = Rc::clone(&do_open_rom); action_open.connect_activate(move |_, _| { do_open_rom(); }); } window.add_action(&action_open); app.set_accels_for_action("win.open", &["o"]); let action_pause = gio::SimpleAction::new("toggle-pause", None); { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); action_pause.connect_activate(move |_, _| { let mut app_state = desktop.borrow_mut(); if app_state.is_loaded() { app_state.toggle_pause(); sync_ui(&app_state, None); } }); } window.add_action(&action_pause); app.set_accels_for_action("win.toggle-pause", &["p"]); let action_reset = gio::SimpleAction::new("reset", None); { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); action_reset.connect_activate(move |_, _| { let mut app_state = desktop.borrow_mut(); if app_state.is_loaded() { app_state.reset(); sync_ui(&app_state, None); } }); } window.add_action(&action_reset); app.set_accels_for_action("win.reset", &["r"]); // --- Keyboard controller for joypad input --- { let desktop = Rc::clone(&desktop); let key_controller = gtk::EventControllerKey::new(); let desktop_for_press = Rc::clone(&desktop); key_controller.connect_key_pressed(move |_, key, _, _| { let mut app_state = desktop_for_press.borrow_mut(); app_state.input_mut().set_key_state(key, true); gtk::glib::Propagation::Proceed }); key_controller.connect_key_released(move |_, key, _, _| { desktop.borrow_mut().input_mut().set_key_state(key, false); }); window.add_controller(key_controller); } // --- Drag-and-drop --- { let desktop = Rc::clone(&desktop); let sync_ui = Rc::clone(&sync_ui); let drop_target = gtk::DropTarget::new(gio::File::static_type(), gdk::DragAction::COPY); drop_target.connect_drop(move |_, value, _, _| { if let Ok(file) = value.get::() { if let Some(path) = file.path() { let mut app_state = desktop.borrow_mut(); if let Err(err) = app_state.load_rom_from_path(&path) { eprintln!("Failed to load ROM '{}': {err}", path.display()); return false; } let name = rom_filename(&path); sync_ui(&app_state, Some(&name)); return true; } } false }); drawing_area.add_controller(drop_target); } // --- Game loop --- { let desktop = Rc::clone(&desktop); let drawing_area = drawing_area.clone(); let frame_for_draw = Rc::clone(&frame_for_draw); glib::timeout_add_local(Duration::from_millis(16), move || { let mut app_state = desktop.borrow_mut(); app_state.tick(); frame_for_draw .borrow_mut() .copy_from_slice(app_state.frame_rgba()); drawing_area.queue_draw(); glib::ControlFlow::Continue }); } window.present(); } fn rom_filename(path: &Path) -> String { path.file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_else(|| "Unknown".into()) } // --------------------------------------------------------------------------- // Input // --------------------------------------------------------------------------- #[derive(Default)] struct InputState { buttons: JoypadButtons, } impl InputState { fn set_key_state(&mut self, key: gdk::Key, pressed: bool) { let button = match key { gdk::Key::Up => JoypadButton::Up, gdk::Key::Down => JoypadButton::Down, gdk::Key::Left => JoypadButton::Left, gdk::Key::Right => JoypadButton::Right, gdk::Key::x | gdk::Key::X => JoypadButton::A, gdk::Key::z | gdk::Key::Z => JoypadButton::B, gdk::Key::Return => JoypadButton::Start, gdk::Key::Shift_L | gdk::Key::Shift_R => JoypadButton::Select, _ => return, }; set_button_pressed(&mut self.buttons, button, pressed); } } impl InputProvider for InputState { fn poll_buttons(&mut self) -> JoypadButtons { self.buttons } } // --------------------------------------------------------------------------- // Audio (stub) // --------------------------------------------------------------------------- #[derive(Default)] struct AudioSink; impl nesemu::AudioOutput for AudioSink { fn push_samples(&mut self, _samples: &[f32]) {} } // --------------------------------------------------------------------------- // Application state // --------------------------------------------------------------------------- struct DesktopApp { host: Option>>, input: InputState, audio: AudioSink, frame_rgba: Vec, state: EmulationState, } impl DesktopApp { fn new() -> Self { Self { host: None, input: InputState::default(), audio: AudioSink, frame_rgba: vec![0; FRAME_RGBA_BYTES], state: EmulationState::Paused, } } fn load_rom_from_path(&mut self, path: &Path) -> Result<(), Box> { let data = std::fs::read(path)?; let runtime = NesRuntime::from_rom_bytes(&data)?; let config = HostConfig::new(SAMPLE_RATE, false); self.host = Some(RuntimeHostLoop::with_config(runtime, config)); self.state = EmulationState::Running; Ok(()) } fn reset(&mut self) { if let Some(host) = self.host.as_mut() { host.runtime_mut().reset(); self.state = EmulationState::Running; } } fn is_loaded(&self) -> bool { self.host.is_some() } fn state(&self) -> EmulationState { self.state } fn toggle_pause(&mut self) { self.state = match self.state { EmulationState::Running => EmulationState::Paused, EmulationState::Paused => EmulationState::Running, _ => EmulationState::Paused, }; } fn tick(&mut self) { if self.state != EmulationState::Running { return; } let Some(host) = self.host.as_mut() else { return; }; let mut null_video = nesemu::NullVideo; if let Err(err) = host.run_frame_unpaced(&mut self.input, &mut null_video, &mut self.audio) { eprintln!("Frame execution error: {err}"); self.state = EmulationState::Paused; return; } self.frame_rgba .copy_from_slice(&host.runtime().frame_rgba()); } fn frame_rgba(&self) -> &[u8] { &self.frame_rgba } fn input_mut(&mut self) -> &mut InputState { &mut self.input } }