Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/target
|
||||
*.nes
|
||||
*.zip
|
||||
4560
Cargo.lock
generated
Normal file
4560
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "nes-emu"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bitfield = "0.19.3"
|
||||
iced = { version = "0.13.1", features = ["debug", "canvas", "tokio", "lazy"] }
|
||||
iced_graphics = { version = "0.13.0", features = ["geometry", "image"] }
|
||||
iced_widget = { version = "0.13.4", features = ["canvas", "image"] }
|
||||
thiserror = "2.0.17"
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
|
||||
45
build.rs
Normal file
45
build.rs
Normal file
@@ -0,0 +1,45 @@
|
||||
use std::{io::Read, process::Stdio};
|
||||
|
||||
fn main() {
|
||||
println!("cargo::rerun-if-changed=src/test_roms/");
|
||||
println!("cargo::rerun-if-changed=build.rs");
|
||||
|
||||
let name = format!(
|
||||
"{}/test-roms",
|
||||
std::env::var("OUT_DIR").expect("Need OUT_DIR env var")
|
||||
);
|
||||
std::fs::create_dir_all(&name).expect("Failed to create rom output dir");
|
||||
println!("cargo::rustc-env=ROM_DIR={name}");
|
||||
|
||||
for file in std::fs::read_dir("./src/test_roms").expect("Failed to read directory") {
|
||||
let file = file.expect("test");
|
||||
let file_name = file.file_name();
|
||||
let file_name = file_name.to_str().unwrap();
|
||||
if let Some(file_name) = file_name.strip_suffix(".asm") {
|
||||
let mut proc = std::process::Command::new("/home/matthew/asm6f/asm6f")
|
||||
.arg(file.file_name())
|
||||
.arg(format!("{name}/{file_name}.nes"))
|
||||
.current_dir("src/test_roms")
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.expect("Failed to start process");
|
||||
let rc = proc.wait();
|
||||
if rc.is_err() || rc.is_ok_and(|s| !s.success()) {
|
||||
let mut stdout = String::new();
|
||||
proc.stdout
|
||||
.unwrap()
|
||||
.read_to_string(&mut stdout)
|
||||
.expect("Failed to read stdout");
|
||||
let mut stderr = String::new();
|
||||
proc.stderr
|
||||
.unwrap()
|
||||
.read_to_string(&mut stderr)
|
||||
.expect("Failed to read stderr");
|
||||
panic!(
|
||||
"Failed to compile {file_name}\n=== STDOUT ===\n{stdout}\n=== STDERR ===\n{stderr}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/apu.rs
Normal file
111
src/apu.rs
Normal file
@@ -0,0 +1,111 @@
|
||||
use tracing::debug;
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct DutyVol(u8);
|
||||
impl Debug;
|
||||
duty, set_duty: 7, 6;
|
||||
r#loop, set_loop: 5;
|
||||
const_vol, set_const_vol: 4;
|
||||
volume, set_volume: 3, 0;
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct Sweep(u8);
|
||||
impl Debug;
|
||||
enable, set_enable: 7;
|
||||
period, set_period: 6, 4;
|
||||
negate, set_negate: 3;
|
||||
shift, set_shift: 2, 0;
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct LengthTimerHigh(u8);
|
||||
impl Debug;
|
||||
length, set_length: 7, 3;
|
||||
timer_high, set_timer_high: 2, 0;
|
||||
}
|
||||
|
||||
struct PulseChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct CounterLoad(u8);
|
||||
impl Debug;
|
||||
halt, set_halt: 7;
|
||||
value, set_value: 6, 0;
|
||||
}
|
||||
|
||||
struct TriangleChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
struct NoiseChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
struct DeltaChannel {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
pub struct APU {
|
||||
pulse_1: PulseChannel,
|
||||
pulse_2: PulseChannel,
|
||||
triangle: TriangleChannel,
|
||||
noise: NoiseChannel,
|
||||
dmc: DeltaChannel,
|
||||
frame_counter: u8,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for APU {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
// write!(
|
||||
// f,
|
||||
// "PPU: f {}, s {}, p {}",
|
||||
// self.frame_count, self.scanline, self.pixel
|
||||
// )
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl APU {
|
||||
pub fn init() -> Self {
|
||||
Self {
|
||||
pulse_1: PulseChannel { enabled: false },
|
||||
pulse_2: PulseChannel { enabled: false },
|
||||
triangle: TriangleChannel { enabled: false },
|
||||
noise: NoiseChannel { enabled: false },
|
||||
dmc: DeltaChannel { enabled: false },
|
||||
frame_counter: 0,
|
||||
}
|
||||
}
|
||||
pub fn read_reg(&mut self, offset: u16) -> u8 {
|
||||
match offset {
|
||||
_ => panic!("No register at {:X}", offset),
|
||||
}
|
||||
}
|
||||
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x15 => {
|
||||
self.dmc.enabled = val & 0b0001_0000 != 0;
|
||||
self.noise.enabled = val & 0b0000_1000 != 0;
|
||||
self.triangle.enabled = val & 0b0000_0100 != 0;
|
||||
self.pulse_2.enabled = val & 0b0000_0010 != 0;
|
||||
self.pulse_1.enabled = val & 0b0000_0001 != 0;
|
||||
}
|
||||
0x11 => {
|
||||
// TODO: load dmc counter with (val & 7F)
|
||||
}
|
||||
_ => panic!("No register at {:X}", offset),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_one_clock_cycle(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn nmi_waiting(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
pub fn irq_waiting(&mut self) -> bool {
|
||||
// TODO: implement logic
|
||||
false
|
||||
}
|
||||
}
|
||||
14
src/controllers.rs
Normal file
14
src/controllers.rs
Normal file
@@ -0,0 +1,14 @@
|
||||
pub struct Controllers {}
|
||||
|
||||
impl Controllers {
|
||||
pub fn init() -> Self {
|
||||
Self {}
|
||||
}
|
||||
pub fn read_joy1(&mut self) -> u8 {
|
||||
0
|
||||
}
|
||||
pub fn read_joy2(&mut self) -> u8 {
|
||||
0
|
||||
}
|
||||
pub fn write_joy_strobe(&mut self, val: u8) { }
|
||||
}
|
||||
944
src/header_menu.rs
Normal file
944
src/header_menu.rs
Normal file
@@ -0,0 +1,944 @@
|
||||
//! Pick lists display a dropdown list of selectable options.
|
||||
//!
|
||||
//! # Example
|
||||
//! ```no_run
|
||||
//! # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
//! #
|
||||
//! use iced::widget::pick_list;
|
||||
//!
|
||||
//! struct State {
|
||||
//! favorite: Option<Fruit>,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
//! enum Fruit {
|
||||
//! Apple,
|
||||
//! Orange,
|
||||
//! Strawberry,
|
||||
//! Tomato,
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug, Clone)]
|
||||
//! enum Message {
|
||||
//! FruitSelected(Fruit),
|
||||
//! }
|
||||
//!
|
||||
//! fn view(state: &State) -> Element<'_, Message> {
|
||||
//! let fruits = [
|
||||
//! Fruit::Apple,
|
||||
//! Fruit::Orange,
|
||||
//! Fruit::Strawberry,
|
||||
//! Fruit::Tomato,
|
||||
//! ];
|
||||
//!
|
||||
//! pick_list(
|
||||
//! fruits,
|
||||
//! state.favorite,
|
||||
//! Message::FruitSelected,
|
||||
//! )
|
||||
//! .placeholder("Select your favorite fruit...")
|
||||
//! .into()
|
||||
//! }
|
||||
//!
|
||||
//! fn update(state: &mut State, message: Message) {
|
||||
//! match message {
|
||||
//! Message::FruitSelected(fruit) => {
|
||||
//! state.favorite = Some(fruit);
|
||||
//! }
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! impl std::fmt::Display for Fruit {
|
||||
//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
//! f.write_str(match self {
|
||||
//! Self::Apple => "Apple",
|
||||
//! Self::Orange => "Orange",
|
||||
//! Self::Strawberry => "Strawberry",
|
||||
//! Self::Tomato => "Tomato",
|
||||
//! })
|
||||
//! }
|
||||
//! }
|
||||
//! ```
|
||||
use iced::alignment;
|
||||
use iced::event::{self, Event};
|
||||
use iced::keyboard;
|
||||
// use iced::layout;
|
||||
use iced::mouse;
|
||||
use iced::overlay;
|
||||
// use iced::renderer;
|
||||
// use iced::text::paragraph;
|
||||
// use iced::text::{self, Text};
|
||||
use iced::touch;
|
||||
// use iced::widget::tree::{self, Tree};
|
||||
use iced::overlay::menu::{self, Menu};
|
||||
use iced::{
|
||||
Background, Border, Color, Element, Length, Padding, Pixels, Point, Rectangle, Size, Theme,
|
||||
Vector,
|
||||
};
|
||||
use iced_graphics::core::text::paragraph;
|
||||
use iced_graphics::core::widget::{Tree, tree};
|
||||
use iced_graphics::core::{Clipboard, Layout, Shell, Text, Widget, layout, renderer, text};
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::f32;
|
||||
|
||||
/// A widget for selecting a single value from a list of options.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::widget::pick_list;
|
||||
///
|
||||
/// struct State {
|
||||
/// favorite: Option<Fruit>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// enum Fruit {
|
||||
/// Apple,
|
||||
/// Orange,
|
||||
/// Strawberry,
|
||||
/// Tomato,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// FruitSelected(Fruit),
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// let fruits = [
|
||||
/// Fruit::Apple,
|
||||
/// Fruit::Orange,
|
||||
/// Fruit::Strawberry,
|
||||
/// Fruit::Tomato,
|
||||
/// ];
|
||||
///
|
||||
/// pick_list(
|
||||
/// fruits,
|
||||
/// state.favorite,
|
||||
/// Message::FruitSelected,
|
||||
/// )
|
||||
/// .placeholder("Select your favorite fruit...")
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::FruitSelected(fruit) => {
|
||||
/// state.favorite = Some(fruit);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl std::fmt::Display for Fruit {
|
||||
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// f.write_str(match self {
|
||||
/// Self::Apple => "Apple",
|
||||
/// Self::Orange => "Orange",
|
||||
/// Self::Strawberry => "Strawberry",
|
||||
/// Self::Tomato => "Tomato",
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct HeaderMenu<
|
||||
'a,
|
||||
T,
|
||||
L,
|
||||
// V,
|
||||
Message,
|
||||
Theme = iced::Theme,
|
||||
Renderer = iced::Renderer,
|
||||
> where
|
||||
T: ToString + PartialEq + Clone,
|
||||
L: Borrow<[T]> + 'a,
|
||||
// V: Borrow<T> + 'a,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
title: String,
|
||||
on_select: Box<dyn Fn(T) -> Message + 'a>,
|
||||
on_open: Option<Message>,
|
||||
on_close: Option<Message>,
|
||||
options: L,
|
||||
placeholder: Option<String>,
|
||||
width: Length,
|
||||
padding: Padding,
|
||||
text_size: Option<Pixels>,
|
||||
text_line_height: text::LineHeight,
|
||||
text_shaping: text::Shaping,
|
||||
font: Option<Renderer::Font>,
|
||||
handle: Handle<Renderer::Font>,
|
||||
class: <Theme as Catalog>::Class<'a>,
|
||||
menu_class: <Theme as menu::Catalog>::Class<'a>,
|
||||
}
|
||||
|
||||
impl<'a, T, L, Message, Theme, Renderer> HeaderMenu<'a, T, L, Message, Theme, Renderer>
|
||||
where
|
||||
T: ToString + PartialEq + Clone,
|
||||
L: Borrow<[T]> + 'a,
|
||||
// V: Borrow<T> + 'a,
|
||||
Message: Clone,
|
||||
Theme: Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
/// Creates a new [`PickList`] with the given list of options, the current
|
||||
/// selected value, and the message to produce when an option is selected.
|
||||
pub fn new(
|
||||
title: impl Into<String>,
|
||||
options: L,
|
||||
// selected: Option<V>,
|
||||
on_click: impl Fn(T) -> Message + 'a,
|
||||
) -> Self {
|
||||
Self {
|
||||
title: title.into(),
|
||||
on_select: Box::new(on_click),
|
||||
on_open: None,
|
||||
on_close: None,
|
||||
options,
|
||||
placeholder: None,
|
||||
// selected,
|
||||
width: Length::Shrink,
|
||||
padding: Padding::new(0.1),
|
||||
text_size: None,
|
||||
text_line_height: text::LineHeight::default(),
|
||||
text_shaping: text::Shaping::default(),
|
||||
font: None,
|
||||
handle: Handle::default(),
|
||||
class: <Theme as Catalog>::default(),
|
||||
menu_class: <Theme as Catalog>::default_menu(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the placeholder of the [`PickList`].
|
||||
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
|
||||
self.placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width of the [`PickList`].
|
||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
||||
self.width = width.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Padding`] of the [`PickList`].
|
||||
pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
|
||||
self.padding = padding.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text size of the [`PickList`].
|
||||
pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
|
||||
self.text_size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the text [`text::LineHeight`] of the [`PickList`].
|
||||
pub fn text_line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
|
||||
self.text_line_height = line_height.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`text::Shaping`] strategy of the [`PickList`].
|
||||
pub fn text_shaping(mut self, shaping: text::Shaping) -> Self {
|
||||
self.text_shaping = shaping;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the font of the [`PickList`].
|
||||
pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
|
||||
self.font = Some(font.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`Handle`] of the [`PickList`].
|
||||
pub fn handle(mut self, handle: Handle<Renderer::Font>) -> Self {
|
||||
self.handle = handle;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`PickList`] is opened.
|
||||
pub fn on_open(mut self, on_open: Message) -> Self {
|
||||
self.on_open = Some(on_open);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the message that will be produced when the [`PickList`] is closed.
|
||||
pub fn on_close(mut self, on_close: Message) -> Self {
|
||||
self.on_close = Some(on_close);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`PickList`].
|
||||
#[must_use]
|
||||
pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
|
||||
where
|
||||
<Theme as Catalog>::Class<'a>: From<StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the [`Menu`].
|
||||
#[must_use]
|
||||
pub fn menu_style(mut self, style: impl Fn(&Theme) -> menu::Style + 'a) -> Self
|
||||
where
|
||||
<Theme as menu::Catalog>::Class<'a>: From<menu::StyleFn<'a, Theme>>,
|
||||
{
|
||||
self.menu_class = (Box::new(style) as menu::StyleFn<'a, Theme>).into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, L, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||
for HeaderMenu<'a, T, L, Message, Theme, Renderer>
|
||||
where
|
||||
T: Clone + ToString + PartialEq + 'a,
|
||||
L: Borrow<[T]>,
|
||||
// V: Borrow<T>,
|
||||
Message: Clone + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
{
|
||||
fn tag(&self) -> tree::Tag {
|
||||
tree::Tag::of::<State<Renderer::Paragraph>>()
|
||||
}
|
||||
|
||||
fn state(&self) -> tree::State {
|
||||
tree::State::new(State::<Renderer::Paragraph>::new())
|
||||
}
|
||||
|
||||
fn size(&self) -> Size<Length> {
|
||||
Size {
|
||||
width: self.width,
|
||||
height: Length::Shrink,
|
||||
}
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&self,
|
||||
tree: &mut Tree,
|
||||
renderer: &Renderer,
|
||||
limits: &layout::Limits,
|
||||
) -> layout::Node {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
let options = self.options.borrow();
|
||||
|
||||
state.options.resize_with(options.len(), Default::default);
|
||||
|
||||
let option_text = Text {
|
||||
content: "",
|
||||
bounds: Size::new(
|
||||
f32::INFINITY,
|
||||
self.text_line_height.to_absolute(text_size).into(),
|
||||
),
|
||||
size: text_size,
|
||||
line_height: self.text_line_height,
|
||||
font,
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: self.text_shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
};
|
||||
|
||||
for (option, paragraph) in options.iter().zip(state.options.iter_mut()) {
|
||||
let label = option.to_string();
|
||||
|
||||
paragraph.update(Text {
|
||||
content: &label,
|
||||
..option_text
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(placeholder) = &self.placeholder {
|
||||
state.placeholder.update(Text {
|
||||
content: placeholder,
|
||||
..option_text
|
||||
});
|
||||
}
|
||||
|
||||
let max_width = match self.width {
|
||||
Length::Shrink => {
|
||||
let labels_width = state.options.iter().fold(0.0, |width, paragraph| {
|
||||
f32::max(width, paragraph.min_width())
|
||||
});
|
||||
|
||||
labels_width.max(
|
||||
self.placeholder
|
||||
.as_ref()
|
||||
.map(|_| state.placeholder.min_width())
|
||||
.unwrap_or(0.0),
|
||||
)
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
let size = {
|
||||
let intrinsic = Size::new(
|
||||
max_width + text_size.0 + self.padding.left,
|
||||
f32::from(self.text_line_height.to_absolute(text_size)),
|
||||
);
|
||||
|
||||
limits
|
||||
.width(self.width)
|
||||
.shrink(self.padding)
|
||||
.resolve(self.width, Length::Shrink, intrinsic)
|
||||
.expand(self.padding)
|
||||
};
|
||||
|
||||
layout::Node::new(size)
|
||||
}
|
||||
|
||||
fn on_event(
|
||||
&mut self,
|
||||
tree: &mut Tree,
|
||||
event: Event,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_renderer: &Renderer,
|
||||
_clipboard: &mut dyn Clipboard,
|
||||
shell: &mut Shell<'_, Message>,
|
||||
_viewport: &Rectangle,
|
||||
) -> event::Status {
|
||||
match event {
|
||||
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
|
||||
| Event::Touch(touch::Event::FingerPressed { .. }) => {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
if state.is_open {
|
||||
// Event wasn't processed by overlay, so cursor was clicked either outside its
|
||||
// bounds or on the drop-down, either way we close the overlay.
|
||||
state.is_open = false;
|
||||
|
||||
if let Some(on_close) = &self.on_close {
|
||||
shell.publish(on_close.clone());
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else if cursor.is_over(layout.bounds()) {
|
||||
// let selected = self.selected.as_ref().map(Borrow::borrow);
|
||||
|
||||
state.is_open = true;
|
||||
// state.hovered_option = self
|
||||
// .options
|
||||
// .borrow()
|
||||
// .iter()
|
||||
// .position(|option| Some(option) == selected);
|
||||
|
||||
if let Some(on_open) = &self.on_open {
|
||||
shell.publish(on_open.clone());
|
||||
}
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse::Event::WheelScrolled {
|
||||
delta: mouse::ScrollDelta::Lines { y, .. },
|
||||
}) => {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
if state.keyboard_modifiers.command()
|
||||
&& cursor.is_over(layout.bounds())
|
||||
&& !state.is_open
|
||||
{
|
||||
fn find_next<'a, T: PartialEq>(
|
||||
selected: &'a T,
|
||||
mut options: impl Iterator<Item = &'a T>,
|
||||
) -> Option<&'a T> {
|
||||
let _ = options.find(|&option| option == selected);
|
||||
|
||||
options.next()
|
||||
}
|
||||
|
||||
let options = self.options.borrow();
|
||||
let selected = None; // self.selected.as_ref().map(Borrow::borrow);
|
||||
|
||||
let next_option = if y < 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter())
|
||||
} else {
|
||||
options.first()
|
||||
}
|
||||
} else if y > 0.0 {
|
||||
if let Some(selected) = selected {
|
||||
find_next(selected, options.iter().rev())
|
||||
} else {
|
||||
options.last()
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(next_option) = next_option {
|
||||
shell.publish((self.on_select)(next_option.clone()));
|
||||
}
|
||||
|
||||
event::Status::Captured
|
||||
} else {
|
||||
event::Status::Ignored
|
||||
}
|
||||
}
|
||||
Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
|
||||
state.keyboard_modifiers = modifiers;
|
||||
|
||||
event::Status::Ignored
|
||||
}
|
||||
_ => event::Status::Ignored,
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_interaction(
|
||||
&self,
|
||||
_tree: &Tree,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
_viewport: &Rectangle,
|
||||
_renderer: &Renderer,
|
||||
) -> mouse::Interaction {
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
|
||||
if is_mouse_over {
|
||||
mouse::Interaction::Pointer
|
||||
} else {
|
||||
mouse::Interaction::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
tree: &Tree,
|
||||
renderer: &mut Renderer,
|
||||
theme: &Theme,
|
||||
_style: &renderer::Style,
|
||||
layout: Layout<'_>,
|
||||
cursor: mouse::Cursor,
|
||||
viewport: &Rectangle,
|
||||
) {
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
// let selected = None; // self.selected.as_ref().map(Borrow::borrow);
|
||||
let state = tree.state.downcast_ref::<State<Renderer::Paragraph>>();
|
||||
|
||||
let bounds = layout.bounds();
|
||||
let is_mouse_over = cursor.is_over(bounds);
|
||||
// let is_selected = selected.is_some();
|
||||
|
||||
let status = if state.is_open {
|
||||
Status::Opened
|
||||
} else if is_mouse_over {
|
||||
Status::Hovered
|
||||
} else {
|
||||
Status::Active
|
||||
};
|
||||
|
||||
let style = Catalog::style(theme, &self.class, status);
|
||||
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
border: style.border,
|
||||
..renderer::Quad::default()
|
||||
},
|
||||
style.background,
|
||||
);
|
||||
|
||||
let handle = match &self.handle {
|
||||
Handle::Arrow { size } => Some((
|
||||
Renderer::ICON_FONT,
|
||||
Renderer::ARROW_DOWN_ICON,
|
||||
*size,
|
||||
text::LineHeight::default(),
|
||||
text::Shaping::Basic,
|
||||
)),
|
||||
Handle::Static(Icon {
|
||||
font,
|
||||
code_point,
|
||||
size,
|
||||
line_height,
|
||||
shaping,
|
||||
}) => Some((*font, *code_point, *size, *line_height, *shaping)),
|
||||
Handle::Dynamic { open, closed } => {
|
||||
if state.is_open {
|
||||
Some((
|
||||
open.font,
|
||||
open.code_point,
|
||||
open.size,
|
||||
open.line_height,
|
||||
open.shaping,
|
||||
))
|
||||
} else {
|
||||
Some((
|
||||
closed.font,
|
||||
closed.code_point,
|
||||
closed.size,
|
||||
closed.line_height,
|
||||
closed.shaping,
|
||||
))
|
||||
}
|
||||
}
|
||||
Handle::None => None,
|
||||
};
|
||||
|
||||
if let Some((font, code_point, size, line_height, shaping)) = handle {
|
||||
let size = size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(
|
||||
Text {
|
||||
content: code_point.to_string(),
|
||||
size,
|
||||
line_height,
|
||||
font,
|
||||
bounds: Size::new(bounds.width, f32::from(line_height.to_absolute(size))),
|
||||
horizontal_alignment: alignment::Horizontal::Right,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
Point::new(
|
||||
bounds.x + bounds.width - self.padding.right,
|
||||
bounds.center_y(),
|
||||
),
|
||||
style.text_color,
|
||||
*viewport,
|
||||
);
|
||||
}
|
||||
|
||||
// let label = None; // selected.map(ToString::to_string);
|
||||
|
||||
let text_size = self.text_size.unwrap_or_else(|| renderer.default_size());
|
||||
|
||||
renderer.fill_text(
|
||||
Text {
|
||||
content: self.title.clone(),
|
||||
size: text_size,
|
||||
line_height: self.text_line_height,
|
||||
font,
|
||||
bounds: Size::new(
|
||||
bounds.width - self.padding.horizontal(),
|
||||
f32::from(self.text_line_height.to_absolute(text_size)),
|
||||
),
|
||||
horizontal_alignment: alignment::Horizontal::Left,
|
||||
vertical_alignment: alignment::Vertical::Center,
|
||||
shaping: self.text_shaping,
|
||||
wrapping: text::Wrapping::default(),
|
||||
},
|
||||
Point::new(bounds.x + self.padding.left, bounds.center_y()),
|
||||
// if is_selected {
|
||||
// style.text_color
|
||||
// } else {
|
||||
style.placeholder_color,
|
||||
// },
|
||||
*viewport,
|
||||
);
|
||||
}
|
||||
|
||||
fn overlay<'b>(
|
||||
&'b mut self,
|
||||
tree: &'b mut Tree,
|
||||
layout: Layout<'_>,
|
||||
renderer: &Renderer,
|
||||
translation: Vector,
|
||||
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
|
||||
let state = tree.state.downcast_mut::<State<Renderer::Paragraph>>();
|
||||
let font = self.font.unwrap_or_else(|| renderer.default_font());
|
||||
|
||||
if state.is_open {
|
||||
let bounds = layout.bounds();
|
||||
|
||||
let on_select = &self.on_select;
|
||||
|
||||
let mut menu = Menu::new(
|
||||
&mut state.menu,
|
||||
self.options.borrow(),
|
||||
&mut state.hovered_option,
|
||||
|option| {
|
||||
state.is_open = false;
|
||||
|
||||
(on_select)(option)
|
||||
},
|
||||
None,
|
||||
&self.menu_class,
|
||||
)
|
||||
.width(bounds.width)
|
||||
.padding(self.padding)
|
||||
.font(font)
|
||||
.text_shaping(self.text_shaping);
|
||||
|
||||
if let Some(text_size) = self.text_size {
|
||||
menu = menu.text_size(text_size);
|
||||
}
|
||||
|
||||
Some(menu.overlay(layout.position() + translation, bounds.height))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T, L, Message, Theme, Renderer> From<HeaderMenu<'a, T, L, Message, Theme, Renderer>>
|
||||
for Element<'a, Message, Theme, Renderer>
|
||||
where
|
||||
T: Clone + ToString + PartialEq + 'a,
|
||||
L: Borrow<[T]> + 'a,
|
||||
// V: Borrow<T> + 'a,
|
||||
Message: Clone + 'a,
|
||||
Theme: Catalog + 'a,
|
||||
Renderer: text::Renderer + 'a,
|
||||
{
|
||||
fn from(pick_list: HeaderMenu<'a, T, L, Message, Theme, Renderer>) -> Self {
|
||||
Self::new(pick_list)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct State<P: text::Paragraph> {
|
||||
menu: menu::State,
|
||||
keyboard_modifiers: keyboard::Modifiers,
|
||||
is_open: bool,
|
||||
hovered_option: Option<usize>,
|
||||
options: Vec<paragraph::Plain<P>>,
|
||||
placeholder: paragraph::Plain<P>,
|
||||
}
|
||||
|
||||
impl<P: text::Paragraph> State<P> {
|
||||
/// Creates a new [`State`] for a [`PickList`].
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
menu: menu::State::default(),
|
||||
keyboard_modifiers: keyboard::Modifiers::default(),
|
||||
is_open: bool::default(),
|
||||
hovered_option: Option::default(),
|
||||
options: Vec::new(),
|
||||
placeholder: paragraph::Plain::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: text::Paragraph> Default for State<P> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// The handle to the right side of the [`PickList`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Handle<Font> {
|
||||
/// Displays an arrow icon (▼).
|
||||
///
|
||||
/// This is the default.
|
||||
Arrow {
|
||||
/// Font size of the content.
|
||||
size: Option<Pixels>,
|
||||
},
|
||||
/// A custom static handle.
|
||||
Static(Icon<Font>),
|
||||
/// A custom dynamic handle.
|
||||
Dynamic {
|
||||
/// The [`Icon`] used when [`PickList`] is closed.
|
||||
closed: Icon<Font>,
|
||||
/// The [`Icon`] used when [`PickList`] is open.
|
||||
open: Icon<Font>,
|
||||
},
|
||||
/// No handle will be shown.
|
||||
None,
|
||||
}
|
||||
|
||||
impl<Font> Default for Handle<Font> {
|
||||
fn default() -> Self {
|
||||
Self::Arrow { size: None }
|
||||
}
|
||||
}
|
||||
|
||||
/// The icon of a [`Handle`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Icon<Font> {
|
||||
/// Font that will be used to display the `code_point`,
|
||||
pub font: Font,
|
||||
/// The unicode code point that will be used as the icon.
|
||||
pub code_point: char,
|
||||
/// Font size of the content.
|
||||
pub size: Option<Pixels>,
|
||||
/// Line height of the content.
|
||||
pub line_height: text::LineHeight,
|
||||
/// The shaping strategy of the icon.
|
||||
pub shaping: text::Shaping,
|
||||
}
|
||||
|
||||
/// The possible status of a [`PickList`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Status {
|
||||
/// The [`PickList`] can be interacted with.
|
||||
Active,
|
||||
/// The [`PickList`] is being hovered.
|
||||
Hovered,
|
||||
/// The [`PickList`] is open.
|
||||
Opened,
|
||||
}
|
||||
|
||||
/// The appearance of a pick list.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Style {
|
||||
/// The text [`Color`] of the pick list.
|
||||
pub text_color: Color,
|
||||
/// The placeholder [`Color`] of the pick list.
|
||||
pub placeholder_color: Color,
|
||||
/// The handle [`Color`] of the pick list.
|
||||
pub handle_color: Color,
|
||||
/// The [`Background`] of the pick list.
|
||||
pub background: Background,
|
||||
/// The [`Border`] of the pick list.
|
||||
pub border: Border,
|
||||
}
|
||||
|
||||
/// The theme catalog of a [`PickList`].
|
||||
pub trait Catalog: menu::Catalog {
|
||||
/// The item class of the [`Catalog`].
|
||||
type Class<'a>;
|
||||
|
||||
/// The default class produced by the [`Catalog`].
|
||||
fn default<'a>() -> <Self as Catalog>::Class<'a>;
|
||||
|
||||
/// The default class for the menu of the [`PickList`].
|
||||
fn default_menu<'a>() -> <Self as menu::Catalog>::Class<'a> {
|
||||
<Self as menu::Catalog>::default()
|
||||
}
|
||||
|
||||
/// The [`Style`] of a class with the given status.
|
||||
fn style(&self, class: &<Self as Catalog>::Class<'_>, status: Status) -> Style;
|
||||
}
|
||||
|
||||
/// A styling function for a [`PickList`].
|
||||
///
|
||||
/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
|
||||
pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
|
||||
|
||||
impl Catalog for Theme {
|
||||
type Class<'a> = StyleFn<'a, Self>;
|
||||
|
||||
fn default<'a>() -> StyleFn<'a, Self> {
|
||||
Box::new(default)
|
||||
}
|
||||
|
||||
fn style(&self, class: &StyleFn<'_, Self>, status: Status) -> Style {
|
||||
class(self, status)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default style of the field of a [`PickList`].
|
||||
pub fn default(theme: &Theme, status: Status) -> Style {
|
||||
let palette = theme.extended_palette();
|
||||
|
||||
let active = Style {
|
||||
text_color: palette.background.weak.text,
|
||||
background: palette.background.weak.color.into(),
|
||||
placeholder_color: palette.background.strong.color,
|
||||
handle_color: palette.background.weak.text,
|
||||
border: Border {
|
||||
radius: 2.0.into(),
|
||||
width: 1.0,
|
||||
color: palette.background.strong.color,
|
||||
},
|
||||
};
|
||||
|
||||
match status {
|
||||
Status::Active => active,
|
||||
Status::Hovered | Status::Opened => Style {
|
||||
border: Border {
|
||||
color: palette.primary.strong.color,
|
||||
..active.border
|
||||
},
|
||||
..active
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new [`PickList`].
|
||||
///
|
||||
/// Pick lists display a dropdown list of selectable options.
|
||||
///
|
||||
/// # Example
|
||||
/// ```no_run
|
||||
/// # mod iced { pub mod widget { pub use iced_widget::*; } pub use iced_widget::Renderer; pub use iced_widget::core::*; }
|
||||
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
|
||||
/// #
|
||||
/// use iced::widget::pick_list;
|
||||
///
|
||||
/// struct State {
|
||||
/// favorite: Option<Fruit>,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
/// enum Fruit {
|
||||
/// Apple,
|
||||
/// Orange,
|
||||
/// Strawberry,
|
||||
/// Tomato,
|
||||
/// }
|
||||
///
|
||||
/// #[derive(Debug, Clone)]
|
||||
/// enum Message {
|
||||
/// FruitSelected(Fruit),
|
||||
/// }
|
||||
///
|
||||
/// fn view(state: &State) -> Element<'_, Message> {
|
||||
/// let fruits = [
|
||||
/// Fruit::Apple,
|
||||
/// Fruit::Orange,
|
||||
/// Fruit::Strawberry,
|
||||
/// Fruit::Tomato,
|
||||
/// ];
|
||||
///
|
||||
/// pick_list(
|
||||
/// fruits,
|
||||
/// state.favorite,
|
||||
/// Message::FruitSelected,
|
||||
/// )
|
||||
/// .placeholder("Select your favorite fruit...")
|
||||
/// .into()
|
||||
/// }
|
||||
///
|
||||
/// fn update(state: &mut State, message: Message) {
|
||||
/// match message {
|
||||
/// Message::FruitSelected(fruit) => {
|
||||
/// state.favorite = Some(fruit);
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// impl std::fmt::Display for Fruit {
|
||||
/// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
/// f.write_str(match self {
|
||||
/// Self::Apple => "Apple",
|
||||
/// Self::Orange => "Orange",
|
||||
/// Self::Strawberry => "Strawberry",
|
||||
/// Self::Tomato => "Tomato",
|
||||
/// })
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn header_menu<'a, T, L, Message, Theme, Renderer>(
|
||||
title: impl Into<String>,
|
||||
options: L,
|
||||
on_selected: impl Fn(T) -> Message + 'a,
|
||||
) -> HeaderMenu<'a, T, L, Message, Theme, Renderer>
|
||||
where
|
||||
T: ToString + PartialEq + Clone + 'a,
|
||||
L: Borrow<[T]> + 'a,
|
||||
Message: Clone,
|
||||
Theme: Catalog + overlay::menu::Catalog,
|
||||
Renderer: text::Renderer,
|
||||
{
|
||||
HeaderMenu::new(title, options, on_selected)
|
||||
}
|
||||
57
src/hex_view.rs
Normal file
57
src/hex_view.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use std::fmt;
|
||||
|
||||
use iced::{widget::{column, lazy, row, text}, Element, Font, Length::Fill, Task};
|
||||
|
||||
pub trait Memory {
|
||||
fn peek(&self, val: u16) -> Option<u8>;
|
||||
fn edit_ver(&self) -> usize;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HexEvent {}
|
||||
|
||||
pub struct HexView {
|
||||
|
||||
}
|
||||
|
||||
struct Val(Option<u8>);
|
||||
impl fmt::Display for Val {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if let Some(val) = self.0 {
|
||||
write!(f, "{:02X}", val)
|
||||
} else {
|
||||
write!(f, "XX")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl HexView {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn render<'a>(&self, mem: &'a impl Memory) -> Element<'a, HexEvent> {
|
||||
struct Row<'a, M>(u16, &'a M);
|
||||
impl<M: Memory> fmt::Display for Row<'_, M> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", Val(self.1.peek(self.0)))?;
|
||||
for i in 1..16 {
|
||||
write!(f, " {}", Val(self.1.peek(self.0 + i)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
column![
|
||||
text!("Hex view"),
|
||||
iced::widget::scrollable(lazy(mem.edit_ver(), |_| column([
|
||||
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F").font(Font::MONOSPACE).into()
|
||||
].into_iter().chain((0..u16::MAX).step_by(16).map(|off| {
|
||||
text!(" {off:04X} | {}", Row(off, mem)).font(Font::MONOSPACE).into()
|
||||
}))))).width(Fill),
|
||||
].width(Fill).into()
|
||||
}
|
||||
|
||||
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
1574
src/lib.rs
Normal file
1574
src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
310
src/main.rs
Normal file
310
src/main.rs
Normal file
@@ -0,0 +1,310 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt,
|
||||
io::{Read, stdin},
|
||||
};
|
||||
|
||||
use iced::{
|
||||
Color, Element, Font,
|
||||
Length::Fill,
|
||||
Point, Renderer, Size, Subscription, Task, Theme, Vector,
|
||||
futures::io::Window,
|
||||
mouse,
|
||||
widget::{
|
||||
Canvas, button,
|
||||
canvas::{Frame, Program},
|
||||
column, container, row, text,
|
||||
},
|
||||
window::{self, Id, Settings},
|
||||
};
|
||||
use nes_emu::{
|
||||
NES, PPU, RenderBuffer,
|
||||
header_menu::header_menu,
|
||||
hex_view::{self, HexEvent, HexView},
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
|
||||
extern crate nes_emu;
|
||||
|
||||
fn main() -> Result<(), iced::Error> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
iced::daemon(Emulator::title, Emulator::update, Emulator::view)
|
||||
.subscription(Emulator::subscriptions)
|
||||
.theme(|_, _| Theme::Dark)
|
||||
.run_with(Emulator::new)
|
||||
|
||||
// iced::application(title, Emulator::update, Emulator::view)
|
||||
// .subscription(Emulator::subscriptions)
|
||||
// .theme(|_| Theme::Dark)
|
||||
// .centered()
|
||||
// .run()
|
||||
}
|
||||
|
||||
enum MemoryTy {
|
||||
Cpu,
|
||||
}
|
||||
|
||||
enum WindowType {
|
||||
Main,
|
||||
Memory(MemoryTy, HexView),
|
||||
TileMap,
|
||||
TileViewer,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
enum HeaderButton {
|
||||
OpenMemory,
|
||||
OpenTileMap,
|
||||
OpenTileViewer,
|
||||
Reset,
|
||||
PowerCycle,
|
||||
}
|
||||
|
||||
impl fmt::Display for HeaderButton {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::OpenMemory => write!(f, "Open Memory Viewer"),
|
||||
Self::OpenTileMap => write!(f, "Open TileMap Viewer"),
|
||||
Self::OpenTileViewer => write!(f, "Open Tile Viewer"),
|
||||
Self::Reset => write!(f, "Reset"),
|
||||
Self::PowerCycle => write!(f, "Power Cycle"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Emulator {
|
||||
nes: NES,
|
||||
windows: HashMap<Id, WindowType>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum Message {
|
||||
Tick(usize),
|
||||
Frame,
|
||||
DMA,
|
||||
CPU,
|
||||
DebugInt,
|
||||
WindowOpened(Id),
|
||||
WindowClosed(Id),
|
||||
Header(HeaderButton),
|
||||
Hex(Id, HexEvent),
|
||||
}
|
||||
|
||||
impl Emulator {
|
||||
fn new() -> (Self, Task<Message>) {
|
||||
let rom_file = concat!(env!("ROM_DIR"), "/", "read_write.nes");
|
||||
// let rom_file = "./Super Mario Bros. (World).nes";
|
||||
let mut nes = nes_emu::NES::load_nes_file(rom_file)
|
||||
.expect("Failed to load nes file");
|
||||
nes.reset();
|
||||
let (win, task) = iced::window::open(Settings::default());
|
||||
(
|
||||
Self {
|
||||
nes,
|
||||
windows: HashMap::from_iter([(win, WindowType::Main)]),
|
||||
},
|
||||
task.map(Message::WindowOpened),
|
||||
)
|
||||
}
|
||||
fn title(&self, win: Id) -> String {
|
||||
match self.windows.get(&win) {
|
||||
Some(WindowType::Main) => "NES emu".into(),
|
||||
Some(WindowType::Memory(_, _)) => "NES MemoryView".into(),
|
||||
Some(WindowType::TileMap) => "NES TileMap".into(),
|
||||
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
|
||||
None => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn open(&mut self, ty: WindowType) -> Task<Message> {
|
||||
let (win, task) = iced::window::open(Settings::default());
|
||||
self.windows.insert(win, ty);
|
||||
return task.map(Message::WindowOpened);
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Tick(count) => {
|
||||
for _ in 0..count {
|
||||
self.nes.run_one_clock_cycle();
|
||||
}
|
||||
}
|
||||
Message::Frame => while !self.nes.run_one_clock_cycle().ppu_frame {},
|
||||
Message::DMA => while !self.nes.run_one_clock_cycle().dma {},
|
||||
Message::CPU => while !self.nes.run_one_clock_cycle().cpu_exec {},
|
||||
Message::DebugInt => while !self.nes.run_one_clock_cycle().dbg_int {},
|
||||
Message::WindowOpened(id) => {
|
||||
// Window
|
||||
}
|
||||
Message::WindowClosed(id) => {
|
||||
if let Some(WindowType::Main) = self.windows.remove(&id) {
|
||||
return iced::exit();
|
||||
}
|
||||
}
|
||||
Message::Header(HeaderButton::OpenMemory) => {
|
||||
return self.open(WindowType::Memory(MemoryTy::Cpu, HexView::new()));
|
||||
}
|
||||
Message::Header(HeaderButton::OpenTileMap) => {
|
||||
return self.open(WindowType::TileMap);
|
||||
}
|
||||
Message::Header(HeaderButton::OpenTileViewer) => {
|
||||
return self.open(WindowType::TileViewer);
|
||||
}
|
||||
Message::Hex(id, val) => {
|
||||
if let Some(WindowType::Memory(_, view)) = self.windows.get_mut(&id) {
|
||||
return view.update(val).map(move |e| Message::Hex(id, e));
|
||||
}
|
||||
}
|
||||
Message::Header(HeaderButton::Reset) => { self.nes.reset(); }
|
||||
Message::Header(HeaderButton::PowerCycle) => { self.nes.power_cycle(); }
|
||||
}
|
||||
// self.image.0.clone_from(self.nes.image());
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn subscriptions(&self) -> Subscription<Message> {
|
||||
window::close_events().map(Message::WindowClosed)
|
||||
}
|
||||
|
||||
fn view(&self, win: Id) -> Element<'_, Message> {
|
||||
match self.windows.get(&win) {
|
||||
Some(WindowType::Main) => {
|
||||
let content = column![
|
||||
self.dropdowns(),
|
||||
Element::from(Canvas::new(self).width(Fill).height(255. * 2.)),
|
||||
self.cpu_state(),
|
||||
self.controls(),
|
||||
]
|
||||
.height(Fill);
|
||||
container(content).width(Fill).height(Fill).into()
|
||||
}
|
||||
Some(WindowType::Memory(ty, view)) => {
|
||||
let hex = match ty {
|
||||
MemoryTy::Cpu => view
|
||||
.render(self.nes.cpu_mem())
|
||||
.map(move |e| Message::Hex(win, e)),
|
||||
};
|
||||
let content = column![hex].width(Fill).height(Fill);
|
||||
container(content).width(Fill).height(Fill).into()
|
||||
}
|
||||
Some(WindowType::TileMap) => {
|
||||
container(Canvas::new(DbgImage::PatternTable(self.nes.ppu()))).width(Fill).height(Fill).into()
|
||||
}
|
||||
Some(WindowType::TileViewer) => {
|
||||
container(Canvas::new(DbgImage::NameTable(self.nes.ppu()))).width(Fill).height(Fill).into()
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cpu_state(&self) -> Element<'_, Message> {
|
||||
row![column![
|
||||
// text!("Registers").font(Font::MONOSPACE),
|
||||
text!("{:?}", self.nes).font(Font::MONOSPACE),
|
||||
],]
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn controls(&self) -> Element<'_, Message> {
|
||||
row![
|
||||
button("Clock tick").on_press(Message::Tick(1)),
|
||||
button("CPU tick").on_press(Message::CPU),
|
||||
button("Next Frame").on_press(Message::Frame),
|
||||
button("Next DMA").on_press(Message::DMA),
|
||||
button("Next DBG").on_press(Message::DebugInt),
|
||||
]
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn dropdowns(&self) -> Element<'_, Message> {
|
||||
row![
|
||||
header_menu(
|
||||
"Console",
|
||||
[
|
||||
HeaderButton::Reset,
|
||||
HeaderButton::PowerCycle,
|
||||
],
|
||||
Message::Header
|
||||
),
|
||||
header_menu(
|
||||
"Debugging",
|
||||
[
|
||||
HeaderButton::OpenMemory,
|
||||
HeaderButton::OpenTileMap,
|
||||
HeaderButton::OpenTileViewer,
|
||||
],
|
||||
Message::Header
|
||||
)
|
||||
]
|
||||
.width(Fill)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Program<Message> for Emulator {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
theme: &Theme,
|
||||
bounds: iced::Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
||||
const SIZE: f32 = 2.;
|
||||
let mut frame = Frame::new(
|
||||
renderer,
|
||||
iced::Size {
|
||||
width: 256. * 2.,
|
||||
height: 240. * 2.,
|
||||
},
|
||||
);
|
||||
frame.scale(2.);
|
||||
for y in 0..240 {
|
||||
for x in 0..256 {
|
||||
let c = self.nes.image().read(y, x);
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32, y as f32),
|
||||
Size::new(1., 1.),
|
||||
Color::from_rgb8(c.r, c.g, c.b),
|
||||
);
|
||||
}
|
||||
}
|
||||
vec![frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
|
||||
enum DbgImage<'a> {
|
||||
NameTable(&'a PPU),
|
||||
PatternTable(&'a PPU),
|
||||
}
|
||||
|
||||
impl Program<Message> for DbgImage<'_> {
|
||||
type State = ();
|
||||
|
||||
fn draw(
|
||||
&self,
|
||||
state: &Self::State,
|
||||
renderer: &Renderer,
|
||||
theme: &Theme,
|
||||
bounds: iced::Rectangle,
|
||||
_cursor: mouse::Cursor,
|
||||
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
||||
const SIZE: f32 = 2.;
|
||||
let mut name_table_frame =
|
||||
Frame::new(renderer, Size::new(256. * 4. + 260. * 2., 256. * 4.));
|
||||
name_table_frame.scale(2.);
|
||||
match self {
|
||||
DbgImage::NameTable(ppu) => ppu.render_name_table(&mut name_table_frame),
|
||||
DbgImage::PatternTable(ppu) => ppu.render_pattern_tables(&mut name_table_frame),
|
||||
}
|
||||
vec![name_table_frame.into_geometry()]
|
||||
}
|
||||
}
|
||||
163
src/mem.rs
Normal file
163
src/mem.rs
Normal file
@@ -0,0 +1,163 @@
|
||||
use crate::hex_view::Memory;
|
||||
|
||||
pub enum Value<'a, R> {
|
||||
Value(u8),
|
||||
Register { reg: &'a R, offset: u16 },
|
||||
}
|
||||
|
||||
impl<R> Value<'_, R> {
|
||||
pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 {
|
||||
match self {
|
||||
Value::Value(v) => v,
|
||||
Value::Register { reg, offset } => f(reg, offset),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Data<R> {
|
||||
RAM(Vec<u8>),
|
||||
ROM(Vec<u8>),
|
||||
Mirror(usize),
|
||||
Reg(R),
|
||||
// Disabled(),
|
||||
}
|
||||
|
||||
pub struct Segment<R> {
|
||||
name: &'static str,
|
||||
position: u16,
|
||||
size: u16,
|
||||
mem: Data<R>,
|
||||
}
|
||||
|
||||
impl<R> Segment<R> {
|
||||
pub fn ram(name: &'static str, position: u16, size: u16) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
size,
|
||||
mem: Data::RAM(vec![0u8; size as usize]),
|
||||
}
|
||||
}
|
||||
pub fn rom(name: &'static str, position: u16, raw: &[u8]) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
size: raw.len() as u16,
|
||||
mem: Data::ROM(Vec::from(raw)),
|
||||
}
|
||||
}
|
||||
pub fn reg(name: &'static str, position: u16, size: u16, reg: R) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
size,
|
||||
mem: Data::Reg(reg),
|
||||
}
|
||||
}
|
||||
pub fn mirror(name: &'static str, position: u16, size: u16, of: usize) -> Self {
|
||||
Self {
|
||||
name,
|
||||
position,
|
||||
size,
|
||||
mem: Data::Mirror(of),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemoryMap<R> {
|
||||
edit_ver: usize,
|
||||
segments: Vec<Segment<R>>,
|
||||
}
|
||||
|
||||
impl<R> MemoryMap<R> {
|
||||
pub fn new(segments: Vec<Segment<R>>) -> Self {
|
||||
Self { edit_ver: 0, segments }
|
||||
}
|
||||
pub fn read(&self, addr: u16) -> Value<'_, R> {
|
||||
// self.edit_ver += 1;
|
||||
for segment in &self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &segment.mem {
|
||||
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
||||
Data::ROM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
||||
Data::Reg(reg) => Value::Register {
|
||||
reg,
|
||||
offset: addr - segment.position,
|
||||
},
|
||||
Data::Mirror(index) => {
|
||||
let offset = addr - segment.position;
|
||||
let s = &self.segments[*index];
|
||||
self.read(s.position + offset % s.size)
|
||||
}
|
||||
// Data::Disabled() => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
todo!("Open bus")
|
||||
}
|
||||
|
||||
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&R, u16, u8)) {
|
||||
self.edit_ver += 1;
|
||||
for segment in &mut self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &mut segment.mem {
|
||||
Data::RAM(items) => {
|
||||
items[(addr - segment.position) as usize] = val;
|
||||
}
|
||||
Data::ROM(_items) => (),
|
||||
Data::Reg(reg) => reg_fn(reg, addr - segment.position, val),
|
||||
Data::Mirror(index) => {
|
||||
let offset = addr - segment.position;
|
||||
let index = *index;
|
||||
let s = &self.segments[index];
|
||||
self.write(s.position + offset % s.size, val, reg_fn)
|
||||
}
|
||||
// Data::Disabled() => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
todo!("Open bus")
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
for s in &mut self.segments {
|
||||
match &mut s.mem {
|
||||
Data::RAM(items) => items.fill(0),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rom(&self, idx: usize) -> Option<&[u8]> {
|
||||
if let Some(Segment { mem: Data::ROM(val), .. }) = self.segments.get(idx) {
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> Memory for MemoryMap<R> {
|
||||
fn peek(&self, addr: u16) -> Option<u8> {
|
||||
for segment in &self.segments {
|
||||
if segment.position <= addr && addr - segment.position < segment.size {
|
||||
return match &segment.mem {
|
||||
Data::RAM(items) => Some(items[(addr - segment.position) as usize]),
|
||||
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
|
||||
Data::Reg(_) => None,
|
||||
Data::Mirror(index) => {
|
||||
let offset = addr - segment.position;
|
||||
let s = &self.segments[*index];
|
||||
self.peek(s.position + offset % s.size)
|
||||
}
|
||||
// Data::Disabled() => todo!(),
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn edit_ver(&self) -> usize {
|
||||
self.edit_ver
|
||||
}
|
||||
}
|
||||
495
src/ppu.rs
Normal file
495
src/ppu.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
use iced::{Point, Size, widget::canvas::Frame};
|
||||
use iced_graphics::geometry::{Fill, Renderer};
|
||||
|
||||
use crate::{hex_view::Memory, mem::{MemoryMap, Segment}};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
impl Into<Fill> for Color {
|
||||
fn into(self) -> Fill {
|
||||
iced::Color::from_rgb8(self.r, self.g, self.b).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderBuffer<const W: usize, const H: usize> {
|
||||
buffer: Box<[Color]>,
|
||||
}
|
||||
|
||||
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, line: usize, pixel: usize, color: Color) {
|
||||
assert!(line < H && pixel < W);
|
||||
self.buffer[line * W + pixel] = color;
|
||||
}
|
||||
pub fn read(&self, line: usize, pixel: usize) -> Color {
|
||||
assert!(line < H && pixel < W);
|
||||
self.buffer[line * W + pixel]
|
||||
}
|
||||
|
||||
pub fn clone_from(&mut self, other: &Self) {
|
||||
self.buffer.copy_from_slice(&other.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PPUMMRegisters {
|
||||
Palette,
|
||||
}
|
||||
|
||||
pub struct OAM {
|
||||
mem: Vec<u8>,
|
||||
addr: u8,
|
||||
}
|
||||
|
||||
enum BackgroundState {
|
||||
NameTableBytePre,
|
||||
NameTableByte,
|
||||
AttrTableBytePre,
|
||||
AttrTableByte,
|
||||
PatternTableTileLowPre,
|
||||
PatternTableTileLow,
|
||||
PatternTableTileHighPre,
|
||||
PatternTableTileHigh,
|
||||
}
|
||||
|
||||
pub struct Background {
|
||||
/// Current vram address, 15 bits
|
||||
v: u16,
|
||||
/// Temp vram address, 15 bits
|
||||
t: u16,
|
||||
/// Fine X control, 3 bits
|
||||
x: u8,
|
||||
/// Whether this is the first or second write to PPUSCROLL
|
||||
/// When false, writes to x
|
||||
w: bool,
|
||||
|
||||
/// When true, v is incremented by 32 after each read
|
||||
vram_column: bool,
|
||||
|
||||
state: BackgroundState,
|
||||
|
||||
cur_shift_high: u8,
|
||||
cur_shift_low: u8,
|
||||
}
|
||||
|
||||
struct Mask {
|
||||
grayscale: bool,
|
||||
background_on_left_edge: bool,
|
||||
sprites_on_left_edge: bool,
|
||||
enable_background: bool,
|
||||
enable_sprites: bool,
|
||||
em_red: bool,
|
||||
em_green: bool,
|
||||
em_blue: bool,
|
||||
}
|
||||
|
||||
pub struct PPU {
|
||||
// registers: PPURegisters,
|
||||
frame_count: usize,
|
||||
nmi_enabled: bool,
|
||||
nmi_waiting: bool,
|
||||
even: bool,
|
||||
scanline: usize,
|
||||
pixel: usize,
|
||||
|
||||
mask: Mask,
|
||||
vblank: bool,
|
||||
|
||||
pub(crate) memory: MemoryMap<PPUMMRegisters>,
|
||||
background: Background,
|
||||
oam: OAM,
|
||||
pub render_buffer: RenderBuffer<256, 240>,
|
||||
pub dbg_int: bool,
|
||||
pub cycle: usize,
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct PPUStatus(u8);
|
||||
impl Debug;
|
||||
vblank, set_vblank: 7;
|
||||
sprite0, set_sprite0: 6;
|
||||
sprite_overflow, set_sprite_overflow: 5;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PPU {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"PPU: f {}, s {}, p {}",
|
||||
self.frame_count, self.scanline, self.pixel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PPU {
|
||||
pub fn with_chr_rom(rom: &[u8]) -> Self {
|
||||
Self {
|
||||
cycle: 25,
|
||||
dbg_int: false,
|
||||
|
||||
mask: Mask {
|
||||
grayscale: false,
|
||||
background_on_left_edge: false,
|
||||
sprites_on_left_edge: false,
|
||||
enable_background: false,
|
||||
enable_sprites: false,
|
||||
em_red: false,
|
||||
em_green: false,
|
||||
em_blue: false,
|
||||
},
|
||||
vblank: false,
|
||||
frame_count: 0,
|
||||
nmi_enabled: false,
|
||||
nmi_waiting: false,
|
||||
even: true, // ??
|
||||
scanline: 0,
|
||||
pixel: 25,
|
||||
render_buffer: RenderBuffer::empty(),
|
||||
memory: MemoryMap::new(vec![
|
||||
Segment::rom("CHR ROM", 0x0000, rom),
|
||||
Segment::ram("Internal VRAM", 0x2000, 0x1000),
|
||||
Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1),
|
||||
Segment::reg("Palette Control", 0x3F00, 0x0100, PPUMMRegisters::Palette),
|
||||
]),
|
||||
background: Background {
|
||||
v: 0,
|
||||
t: 0,
|
||||
x: 0,
|
||||
w: false,
|
||||
vram_column: false,
|
||||
state: BackgroundState::NameTableBytePre,
|
||||
cur_shift_high: 0,
|
||||
cur_shift_low: 0,
|
||||
},
|
||||
oam: OAM {
|
||||
mem: vec![0u8; 256],
|
||||
addr: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self {
|
||||
memory: std::mem::replace(&mut self.memory, MemoryMap::new(vec![])),
|
||||
..Self::with_chr_rom(&[])
|
||||
};
|
||||
}
|
||||
|
||||
pub fn read_reg(&mut self, offset: u16) -> u8 {
|
||||
match offset {
|
||||
0 => panic!("ppuctrl is write-only"),
|
||||
1 => panic!("ppumask is write-only"),
|
||||
2 => {
|
||||
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
|
||||
self.vblank = false;
|
||||
self.background.w = false;
|
||||
tmp
|
||||
}
|
||||
3 => panic!("oamaddr is write-only"),
|
||||
4 => self.oam.mem[self.oam.addr as usize],
|
||||
5 => panic!("ppuscroll is write-only"),
|
||||
6 => panic!("ppuaddr is write-only"),
|
||||
7 => {
|
||||
let val = self.memory.read(self.background.v).reg_map(|a, _| match a {
|
||||
PPUMMRegisters::Palette => todo!(),
|
||||
});
|
||||
// if self.background
|
||||
self.background.v = self
|
||||
.background
|
||||
.v
|
||||
.wrapping_add(if self.background.vram_column { 32 } else { 1 });
|
||||
val
|
||||
}
|
||||
// 7 => self.registers.data,
|
||||
_ => panic!("No register at {:02X}", offset),
|
||||
}
|
||||
}
|
||||
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x00 => {
|
||||
self.nmi_enabled = val & 0b1000_0000 != 0;
|
||||
self.background.t =
|
||||
(self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
|
||||
self.background.vram_column = val & 0b0000_0010 != 0;
|
||||
// TODO: other control fields
|
||||
}
|
||||
0x01 => {
|
||||
// self.dbg_int = true;
|
||||
self.mask.grayscale = val & 0b0000_0001 != 0;
|
||||
self.mask.background_on_left_edge = val & 0b0000_0010 != 0;
|
||||
self.mask.sprites_on_left_edge = val & 0b0000_0100 != 0;
|
||||
self.mask.enable_background = val & 0b0000_1000 != 0;
|
||||
self.mask.enable_background = val & 0b0001_0000 != 0;
|
||||
self.mask.em_red = val & 0b0010_0000 != 0;
|
||||
self.mask.em_green = val & 0b0100_0000 != 0;
|
||||
self.mask.em_blue = val & 0b1000_0000 != 0;
|
||||
// todo!("Mask: {:02X}", val)
|
||||
}
|
||||
0x02 => {
|
||||
todo!("Unable to write to PPU status")
|
||||
// TODO: ppu status
|
||||
}
|
||||
0x03 => self.oam.addr = val,
|
||||
0x04 => {
|
||||
self.oam.mem[self.oam.addr as usize] = val;
|
||||
self.oam.addr = self.oam.addr.wrapping_add(1);
|
||||
}
|
||||
0x05 => {
|
||||
if self.background.w {
|
||||
// Is y scroll
|
||||
self.background.t = (self.background.t & 0b0000_1100_0001_1111)
|
||||
| (((val as u16) << 2) & 0b0000_0011_1110_0000)
|
||||
| (((val as u16) << 12) & 0b0111_0000_0000_0000);
|
||||
self.background.w = false;
|
||||
} else {
|
||||
// Is x scroll
|
||||
self.background.x = val & 0b111; // Lowest three bits
|
||||
self.background.t =
|
||||
(self.background.t & 0b0111_1111_1110_0000) | (val as u16 >> 3);
|
||||
self.background.w = true;
|
||||
}
|
||||
}
|
||||
0x06 => {
|
||||
// TODO: ppu addr
|
||||
if self.background.w {
|
||||
self.background.v =
|
||||
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
|
||||
self.background.w = true;
|
||||
} else {
|
||||
self.background.v =
|
||||
u16::from_le_bytes([self.background.v.to_le_bytes()[0], val & 0b0011_1111]);
|
||||
self.background.w = true;
|
||||
}
|
||||
// todo!("PPUADDR write")
|
||||
}
|
||||
0x07 => {
|
||||
// TODO: ppu data
|
||||
}
|
||||
_ => panic!("No register at {:02X}", offset),
|
||||
}
|
||||
// TODO: use data in PPU
|
||||
}
|
||||
// pub fn write_oamdma(&mut self, val: u8) {
|
||||
// // TODO: OAM high addr
|
||||
// }
|
||||
|
||||
pub fn run_one_clock_cycle(&mut self) -> bool {
|
||||
self.cycle += 1;
|
||||
self.pixel += 1;
|
||||
if self.scanline == 261 && (self.pixel == 341 || (self.pixel == 340 && self.even)) {
|
||||
self.scanline = 0;
|
||||
self.pixel = 0;
|
||||
self.even = !self.even;
|
||||
}
|
||||
if self.pixel == 341 {
|
||||
self.pixel = 0;
|
||||
self.scanline += 1;
|
||||
}
|
||||
if self.mask.enable_background || self.mask.enable_sprites {
|
||||
if self.pixel == 257 {
|
||||
self.background.v = (self.background.v & 0b0111_1011_1110_0000)
|
||||
| (self.background.t & 0b0000_0100_0001_1111);
|
||||
}
|
||||
if self.pixel == 280 && self.scanline == 260 {
|
||||
self.background.v = (self.background.v & 0b0000_0100_0001_1111)
|
||||
| (self.background.t & 0b0111_1011_1110_0000);
|
||||
}
|
||||
if self.scanline < 240 || self.scanline == 261 {
|
||||
// TODO
|
||||
if self.pixel == 0 {
|
||||
// self.dbg_int = true;
|
||||
// idle cycle
|
||||
} else if self.pixel < 257 {
|
||||
// self.dbg_int = true;
|
||||
if self.scanline < 240 {
|
||||
self.render_buffer.write(
|
||||
self.scanline,
|
||||
self.pixel - 1,
|
||||
Color { r: 255, g: 0, b: 0 },
|
||||
); // TODO: this should come from shift registers
|
||||
}
|
||||
self.background.state = match self.background.state {
|
||||
BackgroundState::NameTableByte => {
|
||||
// TODO: Fetch name table byte
|
||||
BackgroundState::AttrTableBytePre
|
||||
}
|
||||
BackgroundState::AttrTableByte => {
|
||||
// TODO: Fetch attr table byte
|
||||
BackgroundState::PatternTableTileLowPre
|
||||
}
|
||||
BackgroundState::PatternTableTileLow => {
|
||||
// TODO: Fetch
|
||||
BackgroundState::PatternTableTileHighPre
|
||||
}
|
||||
BackgroundState::PatternTableTileHigh => {
|
||||
// TODO: Fetch
|
||||
BackgroundState::NameTableBytePre
|
||||
}
|
||||
BackgroundState::NameTableBytePre => BackgroundState::NameTableByte,
|
||||
BackgroundState::AttrTableBytePre => BackgroundState::AttrTableByte,
|
||||
BackgroundState::PatternTableTileLowPre => {
|
||||
BackgroundState::PatternTableTileLow
|
||||
}
|
||||
BackgroundState::PatternTableTileHighPre => {
|
||||
BackgroundState::PatternTableTileHigh
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// TODO: Sprite fetches
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.scanline == 241 && self.pixel == 1 {
|
||||
self.vblank = true;
|
||||
self.nmi_waiting = self.nmi_enabled;
|
||||
self.frame_count += 1;
|
||||
self.background.state = BackgroundState::NameTableBytePre;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn nmi_waiting(&mut self) -> bool {
|
||||
if self.nmi_waiting {
|
||||
self.nmi_waiting = false;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
pub fn irq_waiting(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn render_name_table<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
for y in 0..280 / 8 {
|
||||
for x in 0..512 / 8 {
|
||||
let name = self.memory.peek(x + y * 512 / 8 + 0x2000).unwrap() as u16 * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
for bit in (0..8).rev() {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + bit as f32, y as f32 * 8. + y_off as f32),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn render_pattern_tables<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off + 0x1000).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8 + 0x1000).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32 + 130.),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ppu_registers() {
|
||||
let mut ppu = PPU::with_chr_rom(&[0u8; 8192]);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
assert_eq!(ppu.background.t, 0);
|
||||
assert_eq!(ppu.background.x, 0);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
ppu.write_reg(0, 0);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
ppu.write_reg(0, 0b11);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00000000,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00001111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.write_reg(5, 0x5E);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.read_reg(2);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
}
|
||||
}
|
||||
BIN
src/test_roms/Sprites.pcx
Normal file
BIN
src/test_roms/Sprites.pcx
Normal file
Binary file not shown.
BIN
src/test_roms/Tiles.pcx
Normal file
BIN
src/test_roms/Tiles.pcx
Normal file
Binary file not shown.
26
src/test_roms/basic-cpu.asm
Normal file
26
src/test_roms/basic-cpu.asm
Normal file
@@ -0,0 +1,26 @@
|
||||
; FINAL = ""
|
||||
|
||||
.inesprg 2 ; 2 banks
|
||||
.ineschr 1 ;
|
||||
.inesmap 0 ; mapper 0 = NROM
|
||||
.inesmir 0 ; background mirroring, horizontal
|
||||
|
||||
.org $8000
|
||||
RESET:
|
||||
sed
|
||||
hlt
|
||||
ERROR_:
|
||||
hlt
|
||||
|
||||
IGNORE:
|
||||
rti
|
||||
|
||||
.org $FFFA ; Interrupt vectors go here:
|
||||
.word IGNORE ; NMI
|
||||
.word RESET ; Reset
|
||||
.word IGNORE; IRQ
|
||||
|
||||
;;;; NESASM COMPILER STUFF, ADDING THE PATTERN DATA ;;;;
|
||||
|
||||
.incbin "Sprites.pcx"
|
||||
.incbin "Tiles.pcx"
|
||||
41
src/test_roms/mod.rs
Normal file
41
src/test_roms/mod.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::{NES, hex_view::Memory};
|
||||
|
||||
macro_rules! rom_test {
|
||||
($name:ident, $rom:literal, |$nes:ident| $eval:expr) => {
|
||||
#[test]
|
||||
fn $name() {
|
||||
let rom_file = concat!(env!("ROM_DIR"), "/", $rom);
|
||||
println!("{}: {}", stringify!($name), rom_file);
|
||||
let mut $nes = NES::load_nes_file(rom_file).expect("Failed to create nes object");
|
||||
$nes.reset_and_run_with_timeout(1000000);
|
||||
println!("Final: {:?}", $nes);
|
||||
$eval
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
rom_test!(basic_cpu, "basic-cpu.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8001 HLT :2 []");
|
||||
// Off by one from Mesen, since Mesen doesn't count the clock cycle attempting to execute the 'invalid' opcode
|
||||
assert_eq!(nes.cycle, 11);
|
||||
// This is off by one from Mesen, because Mesen is left pointing at the 'invalid' opcode
|
||||
assert_eq!(nes.cpu.pc, 0x8002);
|
||||
|
||||
assert_eq!(nes.cpu.sp, 0xFD);
|
||||
// assert_eq!(nes.cpu.a, 0x00);
|
||||
// assert_eq!(nes.cpu.x, 0x00);
|
||||
// assert_eq!(nes.cpu.y, 0x00);
|
||||
});
|
||||
|
||||
rom_test!(read_write, "read_write.nes", |nes| {
|
||||
assert_eq!(nes.last_instruction, "0x8011 HLT :2 []");
|
||||
assert_eq!(nes.cycle, 31);
|
||||
assert_eq!(nes.cpu.pc, 0x8012);
|
||||
assert_eq!(nes.cpu.sp, 0xFD);
|
||||
assert_eq!(nes.cpu.a, 0xAA);
|
||||
assert_eq!(nes.cpu.x, 0xAA);
|
||||
assert_eq!(nes.cpu.y, 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0000).unwrap(), 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0001).unwrap(), 0xAA);
|
||||
assert_eq!(nes.cpu_mem().peek(0x0002).unwrap(), 0xAA);
|
||||
});
|
||||
31
src/test_roms/read_write.asm
Normal file
31
src/test_roms/read_write.asm
Normal file
@@ -0,0 +1,31 @@
|
||||
; FINAL = ""
|
||||
|
||||
.inesprg 2 ; 2 banks
|
||||
.ineschr 1 ;
|
||||
.inesmap 0 ; mapper 0 = NROM
|
||||
.inesmir 0 ; background mirroring, horizontal
|
||||
|
||||
.org $8000
|
||||
RESET:
|
||||
lda #$aa
|
||||
sta $0000
|
||||
ldx $0000
|
||||
ldy $0000
|
||||
stx $0001
|
||||
sty $0002
|
||||
hlt
|
||||
ERROR_:
|
||||
hlt
|
||||
|
||||
IGNORE:
|
||||
rti
|
||||
|
||||
.org $FFFA ; Interrupt vectors go here:
|
||||
.word IGNORE ; NMI
|
||||
.word RESET ; Reset
|
||||
.word IGNORE; IRQ
|
||||
|
||||
;;;; NESASM COMPILER STUFF, ADDING THE PATTERN DATA ;;;;
|
||||
|
||||
.incbin "Sprites.pcx"
|
||||
.incbin "Tiles.pcx"
|
||||
Reference in New Issue
Block a user