Add menu for loading a ROM file

This commit is contained in:
FyloZ 2025-06-28 19:33:52 -04:00
parent 7599fea8c9
commit cb0e7b0381
13 changed files with 517 additions and 70 deletions

153
Cargo.lock generated
View File

@ -14,6 +14,12 @@ version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.1"
@ -38,6 +44,48 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "dialog"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736bab36d647d14c985725a57a4110a1182c6852104536cd42f1c97e96d29bf0"
dependencies = [
"dirs",
"rpassword",
]
[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi 0.3.9",
]
[[package]]
name = "getrandom"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
dependencies = [
"cfg-if 1.0.1",
"libc",
"wasi",
]
[[package]]
name = "heck"
version = "0.5.0"
@ -50,6 +98,16 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
"winapi 0.2.8",
"winapi-build",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -62,6 +120,16 @@ version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "libredox"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1580801010e535496706ba011c15f8532df6b42297d2e471fec38ceadd8c0638"
dependencies = [
"bitflags 2.8.0",
"libc",
]
[[package]]
name = "log"
version = "0.4.25"
@ -113,6 +181,28 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "rpassword"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37473170aedbe66ffa3ad3726939ba677d83c646ad4fd99e5b4bc38712f45ec"
dependencies = [
"kernel32-sys",
"libc",
"winapi 0.2.8",
]
[[package]]
name = "rustversion"
version = "1.0.19"
@ -137,7 +227,7 @@ version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3"
dependencies = [
"cfg-if",
"cfg-if 1.0.1",
"libc",
"version-compare",
]
@ -216,6 +306,26 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "time"
version = "0.3.37"
@ -254,6 +364,7 @@ name = "ui"
version = "0.1.0"
dependencies = [
"core",
"dialog",
"sdl2",
"simplelog",
"uitlk",
@ -278,6 +389,40 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
@ -287,6 +432,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.59.0"

View File

@ -29,6 +29,11 @@ impl CpuMemoryBus {
}
}
pub fn reset(&mut self) {
self.ram = [0; RAM_SIZE];
self.mapper = None;
}
/// Register a mapper in the memory bus, simulates plugging in a cartridge.
pub fn register_mapper(&mut self, mapper: Box<dyn Mapper>) {
self.mapper = Some(mapper);

View File

@ -155,6 +155,19 @@ impl Cpu {
nmi_requested: false,
}
}
pub fn reset(&mut self) {
self.registers.pc = 0x8000;
self.registers.sp = 0xFD;
self.registers.a = 0;
self.registers.x = 0;
self.registers.y = 0;
self.registers.status = 0x04;
self.cycle = 0;
self.busy_cycle_count = 0;
self.oam_dma_triggered = false;
self.nmi_requested = false;
}
pub fn initialize(&mut self) {
let init_pc = self.memory_bus.borrow().get_word(INITIAL_PC_ADDRESS);

View File

@ -31,6 +31,13 @@ impl PpuMemoryBus {
}
}
pub fn reset(&mut self) {
self.mapper = None;
self.nametable_0 = [0u8; NAMETABLE_SIZE];
self.nametable_1 = [0u8; NAMETABLE_SIZE];
self.palette = [0u8; PALETTE_TABLE_SIZE];
}
fn read_nametable(&self, addr: u16) -> u8 {
assert!(addr >= 0x2000);
assert!(addr < 0x3000);

View File

@ -41,6 +41,24 @@ impl Ppu {
}
}
pub fn reset(&mut self) {
self.registers.control = 0;
self.registers.mask = 0;
self.registers.status = 0;
self.registers.oam_addr = 0;
self.registers.oam_data = 0;
self.registers.scroll = 0;
self.registers.addr = 0;
self.registers.data = 0;
self.registers.oam_dma = 0;
self.internal_registers.v = 0;
self.internal_registers.t = 0;
self.internal_registers.x = 0;
self.internal_registers.w = false;
self.memory_bus.reset();
}
pub fn get_background_pattern_table_addr(&self) -> u16 {
let control = self.registers.control & PpuControl::BackgroundPatternTableAddress.bits();
(control as u16) << 8

View File

@ -80,7 +80,7 @@ pub trait RomLoader {
}
impl Rom {
pub fn read(path: &str) -> Result<Rom, RomReadError> {
pub fn read(path: String) -> Result<Rom, RomReadError> {
info!("ROM - Reading file from {}", path);
let content = match fs::read(path) {

View File

@ -1,12 +1,12 @@
use crate::cpu::memory::CpuMemoryBus;
use crate::cpu::Cpu;
use crate::mappers::get_mapper;
use crate::ppu::Ppu;
use crate::rom::{Rom, RomReadError};
use crate::{Clock, Observable};
use simplelog::info;
use std::cell::RefCell;
use std::rc::Rc;
use crate::cpu::Cpu;
use crate::mappers::get_mapper;
use crate::cpu::memory::CpuMemoryBus;
use crate::rom::{Rom, RomReadError};
use crate::{Clock, Observable};
use crate::ppu::Ppu;
const CPU_CLOCK_DIVISOR: usize = 12;
const CPU_CYCLE_PER_REFRESH: usize = MASTER_CYCLE_PER_REFRESH / CPU_CLOCK_DIVISOR;
@ -19,7 +19,8 @@ const REFRESH_RATE: usize = 60; // 60 Hz
pub struct System {
cpu: Cpu,
memory_bus: Rc<RefCell<CpuMemoryBus>>,
ppu: Rc<RefCell<Ppu>>
ppu: Rc<RefCell<Ppu>>,
initialized: bool
}
impl System {
@ -27,19 +28,32 @@ impl System {
let memory_bus = Rc::new(RefCell::new(CpuMemoryBus::new()));
let cpu = Cpu::new(Rc::clone(&memory_bus));
let ppu = Rc::new(RefCell::new(Ppu::new()));
let weak_ppu = Rc::downgrade(&ppu);
memory_bus.borrow_mut().register_observer(weak_ppu);
Self { cpu, memory_bus, ppu }
Self {
cpu,
memory_bus,
ppu,
initialized: false
}
}
pub fn insert_rom(&mut self, path: &str) -> Result<(), RomReadError> {
pub fn reset(&mut self) {
self.cpu.reset();
self.memory_bus.borrow_mut().reset();
self.ppu.borrow_mut().reset();
self.initialized = false;
}
pub fn insert_rom(&mut self, path: String) -> Result<(), RomReadError> {
let rom = Rom::read(path)?;
let mapper = get_mapper(rom);
self.memory_bus.borrow_mut().register_mapper(mapper);
self.cpu.initialize();
self.initialized = true;
info!("Successfully inserted ROM into system");
Ok(())
@ -48,6 +62,10 @@ impl System {
impl Clock for System {
fn cycle(&mut self) {
if !self.initialized {
return;
}
for _cpu_cycle in 0usize..CPU_CYCLE_PER_REFRESH {
self.cpu.cycle();

BIN
nintendo-nes-font.ttf Normal file

Binary file not shown.

View File

@ -8,5 +8,6 @@ edition = "2021"
[dependencies]
core = { path = "../core" }
uitlk = { path = "../uitlk" }
sdl2 = "0.37.0"
dialog = "0.3.0"
sdl2 = { version = "0.37.0", features = ["ttf"] }
simplelog = { version = "^0.12.0", features = ["paris"] }

162
ui/src/components/menu.rs Normal file
View File

@ -0,0 +1,162 @@
use crate::components::{ButtonComponent, MouseListener, Text, UIComponent};
use crate::WINDOW_SCALE;
use core::system::System;
use dialog::{DialogBox, FileSelectionMode};
use sdl2::pixels::PixelFormatEnum;
use sdl2::rect::Rect;
use sdl2::render::{Texture, TextureCreator, WindowCanvas};
use sdl2::ttf::Font;
use sdl2::video::WindowContext;
use simplelog::warn;
use std::cell::RefCell;
use std::rc::Rc;
const BAR_WIDTH: u32 = 256 * WINDOW_SCALE;
const BAR_HEIGHT: u32 = 8 * WINDOW_SCALE;
const MENU_MARGIN: u32 = 2 * WINDOW_SCALE;
const MENU_PIXEL_FORMAT: PixelFormatEnum = PixelFormatEnum::RGB24;
const BAR_COLOR: [u8; 3] = [0x35, 0x35, 0x35];
const HIGHLIGHT_COLOR: [u8; 3] = [0x4d, 0x4d, 0x4d];
pub struct MenuBar<'a> {
bar_texture: Texture<'a>,
load_rom_menu: Menu<'a>,
system: Rc<RefCell<System>>,
}
impl MenuBar<'_> {
pub fn new<'a>(
texture_creator: &'a TextureCreator<WindowContext>,
font: &Font,
system: Rc<RefCell<System>>,
) -> Result<MenuBar<'a>, String> {
let mut bar_texture = texture_creator
.create_texture_static(MENU_PIXEL_FORMAT, 1, 1)
.map_err(|e| e.to_string())?;
bar_texture
.update(None, &BAR_COLOR, BAR_COLOR.len())
.map_err(|e| e.to_string())?;
let load_rom_menu = Menu::new("LOAD ROM", font, texture_creator)?;
Ok(MenuBar {
bar_texture,
load_rom_menu,
system,
})
}
pub fn draw(&mut self, canvas: &mut WindowCanvas) -> Result<(), String> {
self.draw_bar(canvas)?;
self.load_rom_menu.render(0, canvas)
}
fn draw_bar(&mut self, canvas: &mut WindowCanvas) -> Result<(), String> {
let rect = Rect::new(0, 0, BAR_WIDTH, BAR_HEIGHT);
canvas.copy(&self.bar_texture, None, rect)
}
fn select_rom(&self) {
let mut system = self.system.borrow_mut();
system.reset();
let choice = dialog::FileSelection::new("Load ROM")
.mode(FileSelectionMode::Open)
.show()
.expect("Could not display file selection");
match choice {
None => {}
Some(file_path) => {
let insertion_result = system.insert_rom(file_path);
if insertion_result.is_err() {
warn!("Failed to insert ROM: {}", insertion_result.err().unwrap());
dialog::Message::new("Failed to insert ROM")
.show()
.expect("Could not show message");
}
}
}
}
}
impl MouseListener for MenuBar<'_> {
fn mouse_move(&mut self, x: i32, y: i32) {
if y > BAR_HEIGHT as i32 {
self.load_rom_menu.set_hover(false);
}
let hover = self.load_rom_menu.rect.contains_point((x, y));
self.load_rom_menu.set_hover(hover);
}
fn mouse_down(&mut self) {
if self.load_rom_menu.hover() {
self.select_rom();
}
}
}
pub struct Menu<'a> {
highlight_texture: Texture<'a>,
hover: bool,
pub rect: Rect,
text: Text<'a>,
}
impl<'a> Menu<'a> {
pub fn new(
label: &'static str,
font: &Font,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Result<Self, String> {
let text = Text::new(label, font, texture_creator)?;
let rect = Rect::new(0, 0, text.width() + MENU_MARGIN * 2, BAR_HEIGHT);
let mut highlight_texture = texture_creator
.create_texture_static(MENU_PIXEL_FORMAT, 1, 1)
.map_err(|e| e.to_string())?;
highlight_texture
.update(None, &HIGHLIGHT_COLOR, HIGHLIGHT_COLOR.len())
.map_err(|e| e.to_string())?;
Ok(Menu {
highlight_texture,
hover: false,
rect,
text,
})
}
pub fn render(&mut self, x: u32, canvas: &mut WindowCanvas) -> Result<(), String> {
self.rect.set_x(x as i32);
if self.hover {
// Only draw the highlight texture if the menu is hovered
canvas.copy(&self.highlight_texture, None, self.rect)?;
}
self.text.render_center(self.rect, canvas)
}
}
impl UIComponent for Menu<'_> {
fn width(&self) -> u32 {
self.rect.width()
}
fn height(&self) -> u32 {
self.rect.height()
}
}
impl ButtonComponent for Menu<'_> {
fn hover(&self) -> bool {
self.hover
}
fn set_hover(&mut self, hover: bool) {
self.hover = hover;
}
}

92
ui/src/components/mod.rs Normal file
View File

@ -0,0 +1,92 @@
pub mod menu;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::{Texture, TextureCreator, WindowCanvas};
use sdl2::ttf::Font;
use sdl2::video::WindowContext;
const TEXT_COLOR: Color = Color::WHITE;
pub trait UIComponent {
/// The width of the component in pixels
fn width(&self) -> u32;
/// The height of the component in pixels
fn height(&self) -> u32;
}
pub trait ButtonComponent {
/// If the component is hovered
fn hover(&self) -> bool;
/// Set the hover status of the component
fn set_hover(&mut self, hover: bool);
}
pub trait MouseListener {
/// Called when the mouse moves in the window.
///
/// # Arguments
/// - `x`: The horizontal position of the mouse (left to right)
/// - `y`: The vertical position of the mouse (top to bottom)
fn mouse_move(&mut self, x: i32, y: i32);
/// Called when the left button of the mouse is clicked.
fn mouse_down(&mut self);
}
pub struct Text<'a> {
rect: Rect,
texture: Texture<'a>,
}
impl<'a> Text<'a> {
/// Creates a new text
///
/// # Arguments
/// * `str` - The string to be contained by the text
/// * `font` - A reference to the font to use
/// * `texture_creator` - A reference to a texture creator
pub fn new(
str: &'static str,
font: &Font,
texture_creator: &'a TextureCreator<WindowContext>,
) -> Result<Self, String> {
let surface = font
.render(str)
.solid(TEXT_COLOR)
.map_err(|e| e.to_string())?;
let texture = surface
.as_texture(texture_creator)
.map_err(|e| e.to_string())?;
let rect = Rect::new(0, 0, surface.width(), surface.height());
Ok(Text { rect, texture })
}
/// Renders the text at center of a rectangle
///
/// # Arguments
/// * `destination` - The destination rectangle to draw the text to
/// * `canvas` - The canvas on which the text will be rendered
pub fn render_center(
&self,
destination: Rect,
canvas: &mut WindowCanvas,
) -> Result<(), String> {
let centered_rect = self.rect.centered_on(destination.center());
canvas.copy(&self.texture, None, centered_rect)
}
/// The width of the text in pixels
pub fn width(&self) -> u32 {
self.rect.width()
}
/// The heights of the text in pixels
pub fn height(&self) -> u32 {
self.rect.height()
}
}

View File

@ -1,11 +1,17 @@
extern crate sdl2;
mod renderer;
mod components;
use crate::renderer::Renderer;
use crate::components::menu::MenuBar;
use crate::components::MouseListener;
use core::system::System;
use core::Clock;
use sdl2::event::Event;
use sdl2::rect::Point;
use sdl2::mouse::MouseButton;
use sdl2::pixels::Color;
use simplelog::*;
use std::cell::RefCell;
use std::fs::File;
use std::rc::Rc;
use std::time::Duration;
const ROM_PATH: &'static str = "./roms/dk.nes";
@ -32,7 +38,12 @@ fn main() -> Result<(), String> {
])
.unwrap();
let system = Rc::new(RefCell::new(System::new()));
let sdl_context = sdl2::init()?;
let ttf_context = sdl2::ttf::init().map_err(|e| e.to_string())?;
let font = ttf_context.load_font("./nintendo-nes-font.ttf", 16)?;
let video_subsystem = sdl_context.video()?;
let window = video_subsystem
@ -42,38 +53,40 @@ fn main() -> Result<(), String> {
.build()
.map_err(|e| e.to_string())?;
let mut renderer = Renderer::new(window, WINDOW_SCALE)?;
let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
let texture_creator = canvas.texture_creator();
let mut menu = MenuBar::new(&texture_creator, &font, Rc::clone(&system))?;
let mut event_pump = sdl_context.event_pump()?;
'running: loop {
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. } => break 'running,
Event::MouseMotion { x, y, .. } => {
menu.mouse_move(x, y);
}
Event::MouseButtonUp {
mouse_btn: MouseButton::Left,
..
} => {
menu.mouse_down();
}
_ => {}
}
}
system.borrow_mut().cycle();
canvas.set_draw_color(Color::BLACK);
canvas.clear();
menu.draw(&mut canvas)?;
canvas.present();
renderer.draw(&Point::new(1, 1))?;
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
// Main loop
}
Ok(())
// let mut window = UIWindow::new("NesRust", 800, 600, 60);
//
// // let mut system = System::new();
// // system.insert_rom(ROM_PATH).expect("Failed to insert ROM");
// loop {
// // system.cycle();
// if window.poll_events() {
// break;
// }
//
// // TODO: System loop
//
// window.render();
// window.wait();
// }
}

View File

@ -1,33 +0,0 @@
use sdl2::pixels::Color;
use sdl2::rect::{Point, Rect};
use sdl2::render::WindowCanvas;
use sdl2::video::Window;
pub struct Renderer {
canvas: WindowCanvas,
scale: u32,
}
impl Renderer {
pub fn new(window: Window, scale: u32) -> Result<Renderer, String> {
let canvas = window.into_canvas().build().map_err(|e| e.to_string())?;
Ok(Renderer { canvas, scale })
}
pub fn draw(&mut self, point: &Point) -> Result<(), String> {
self.canvas.set_draw_color(Color::BLACK);
self.canvas.clear();
self.canvas.set_draw_color(Color::GREEN);
self.canvas.fill_rect(Rect::new(
point.x() * self.scale as i32,
point.y() * self.scale as i32,
self.scale,
self.scale,
))?;
self.canvas.present();
Ok(())
}
}