Files
nesemu/crates/nesemu-desktop/src/main.rs
se.cherkasov 38a62b6f93
Some checks failed
CI / rust (push) Has been cancelled
refactor(desktop): decompose monolithic main.rs into layered modules
Split DesktopApp into input, audio, video, scheduling, and app modules.
Migrate DesktopApp from manual pause/resume logic to library ClientRuntime.
2026-03-18 12:52:08 +03:00

402 lines
13 KiB
Rust

mod app;
mod audio;
mod input;
mod scheduling;
mod video;
use std::cell::RefCell;
use std::path::{Path, PathBuf};
use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicU32, Ordering as AtomicOrdering};
use std::time::Instant;
use gtk::gdk;
use gtk::gio;
use gtk::glib;
use gtk::prelude::*;
use gtk4 as gtk;
use nesemu::prelude::EmulationState;
use nesemu::{FRAME_HEIGHT, FRAME_RGBA_BYTES, FRAME_WIDTH};
use app::DesktopApp;
use scheduling::DesktopFrameScheduler;
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<RefCell<Option<PathBuf>>> =
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: &gtk::Application, initial_rom: Option<PathBuf>) {
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();
let volume = Arc::new(AtomicU32::new(f32::to_bits(0.75)));
let volume_scale = gtk::Scale::with_range(gtk::Orientation::Horizontal, 0.0, 1.0, 0.05);
volume_scale.set_value(0.75);
volume_scale.set_draw_value(false);
volume_scale.set_width_request(100);
volume_scale.set_tooltip_text(Some("Volume"));
volume_scale.set_focusable(false);
{
let volume = Arc::clone(&volume);
volume_scale.connect_value_changed(move |scale| {
let val = scale.value() as f32;
volume.store(f32::to_bits(val), AtomicOrdering::Relaxed);
});
}
header.pack_start(&open_button);
header.pack_start(&pause_button);
header.pack_start(&reset_button);
let volume_box = gtk::Box::new(gtk::Orientation::Horizontal, 4);
let volume_icon = gtk::Image::from_icon_name("audio-volume-high-symbolic");
volume_box.append(&volume_icon);
volume_box.append(&volume_scale);
header.pack_end(&volume_box);
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(Arc::clone(&volume))));
let frame_for_draw: Rc<RefCell<Vec<u8>>> = Rc::new(RefCell::new(vec![0u8; FRAME_RGBA_BYTES]));
let scheduler = Rc::new(RefCell::new(DesktopFrameScheduler::new()));
// --- 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();
video::draw_frame(&frame, cr, width, height);
});
}
// --- 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 scheduler = Rc::clone(&scheduler);
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 scheduler = Rc::clone(&scheduler);
let sync_ui = Rc::clone(&sync_ui);
chooser.connect_response(move |dialog, response| {
if response == gtk::ResponseType::Accept
&& 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 {
scheduler.borrow_mut().reset_timing();
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 scheduler = Rc::clone(&scheduler);
let sync_ui = Rc::clone(&sync_ui);
pause_button.connect_clicked(move |_| {
let mut app_state = desktop.borrow_mut();
app_state.toggle_pause();
scheduler.borrow_mut().reset_timing();
sync_ui(&app_state, None);
});
}
{
let desktop = Rc::clone(&desktop);
let scheduler = Rc::clone(&scheduler);
let sync_ui = Rc::clone(&sync_ui);
reset_button.connect_clicked(move |_| {
let mut app_state = desktop.borrow_mut();
app_state.reset();
scheduler.borrow_mut().reset_timing();
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", &["<Ctrl>o"]);
let action_pause = gio::SimpleAction::new("toggle-pause", None);
{
let desktop = Rc::clone(&desktop);
let scheduler = Rc::clone(&scheduler);
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();
scheduler.borrow_mut().reset_timing();
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 scheduler = Rc::clone(&scheduler);
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();
scheduler.borrow_mut().reset_timing();
sync_ui(&app_state, None);
}
});
}
window.add_action(&action_reset);
app.set_accels_for_action("win.reset", &["<Ctrl>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 scheduler = Rc::clone(&scheduler);
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::<gio::File>()
&& 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;
}
scheduler.borrow_mut().reset_timing();
let name = rom_filename(&path);
sync_ui(&app_state, Some(&name));
return true;
}
false
});
drawing_area.add_controller(drop_target);
}
// --- Game loop ---
{
schedule_game_loop(
Rc::clone(&desktop),
drawing_area.clone(),
Rc::clone(&frame_for_draw),
Rc::clone(&scheduler),
);
}
window.present();
}
fn schedule_game_loop(
desktop: Rc<RefCell<DesktopApp>>,
drawing_area: gtk::DrawingArea,
frame_for_draw: Rc<RefCell<Vec<u8>>>,
scheduler: Rc<RefCell<DesktopFrameScheduler>>,
) {
let interval = desktop.borrow().frame_interval();
let delay = scheduler
.borrow_mut()
.delay_until_next_frame(Instant::now(), interval);
glib::timeout_add_local_once(delay, move || {
{
let mut app_state = desktop.borrow_mut();
let now = Instant::now();
let interval = app_state.frame_interval();
scheduler.borrow_mut().mark_frame_complete(now, interval);
app_state.tick();
frame_for_draw
.borrow_mut()
.copy_from_slice(app_state.frame_rgba());
drawing_area.queue_draw();
}
schedule_game_loop(desktop, drawing_area, frame_for_draw, scheduler);
});
}
fn rom_filename(path: &Path) -> String {
path.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "Unknown".into())
}