diff --git a/Cargo.lock b/Cargo.lock index 75b5b12..14022ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2196,7 +2196,9 @@ name = "nes-emu" version = "0.1.0" dependencies = [ "bitfield", + "bytes", "iced", + "iced_core", "rfd", "thiserror 2.0.17", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 39abda1..ac3727f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" bitfield = "0.19.3" # iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] } iced = { path = "../iced", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] } +iced_core = { path = "../iced/core", features = ["advanced"] } rfd = "0.17.2" # iced_graphics = { version = "0.14.0", features = ["geometry", "image"] } # iced_widget = { version = "0.13.4", features = ["canvas", "image"] } @@ -14,3 +15,4 @@ thiserror = "2.0.17" tokio = { version = "1.48.0", features = ["full"] } tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] } +bytes = "*" diff --git a/src/apu.rs b/src/apu.rs index c92a3ae..9746b44 100644 --- a/src/apu.rs +++ b/src/apu.rs @@ -160,6 +160,11 @@ impl APU { }, } } + + pub fn reset(&mut self) { + *self = Self::init(); + } + pub fn read_reg(&mut self, offset: u16) -> u8 { // println!("APU read: {offset:02X}"); match offset { @@ -177,6 +182,7 @@ impl APU { _ => panic!("No register at {:X}", offset), } } + pub fn write_reg(&mut self, offset: u16, val: u8) { // println!("APU write: {offset:02X} <= {val:02X}"); match offset { @@ -238,6 +244,7 @@ impl APU { self.pulse_1.q_frame_clock(); self.pulse_2.q_frame_clock(); // TODO: clock all } + fn h_frame_clock(&mut self) { self.pulse_1.q_frame_clock(); self.pulse_1.h_frame_clock(); @@ -271,15 +278,19 @@ impl APU { } false } + pub fn peek_nmi(&self) -> bool { false } + pub fn nmi_waiting(&mut self) -> bool { false } + pub fn peek_irq(&self) -> bool { self.frame_counter.irq } + pub fn irq_waiting(&mut self) -> bool { self.frame_counter.irq } diff --git a/src/controllers.rs b/src/controllers.rs index 34181b1..37e4c79 100644 --- a/src/controllers.rs +++ b/src/controllers.rs @@ -1,15 +1,98 @@ +use bitfield::bitfield; +bitfield! { + #[derive(PartialEq, Eq, Hash, Clone, Copy)] + pub struct ControllerState(u8); + impl Debug; + pub a, set_a: 0; + pub b, set_b: 1; + pub select, set_select: 2; + pub start, set_start: 3; + pub up, set_up: 4; + pub down, set_down: 5; + pub left, set_left: 6; + pub right, set_right: 7; +} + +#[derive(Debug, Clone, Copy)] +struct ControllerShiftReg { + state: ControllerState, + shift: u8, +} + +impl ControllerShiftReg { + fn new() -> Self { + Self { + state: ControllerState(0), + shift: 0, + } + } + + fn read(&mut self) -> u8 { + let val = self.shift & 1; + // Reads after the first 8 are always 1 + self.shift = (self.shift >> 1) | 0x80; + val + } + + fn load(&mut self) { + self.shift = self.state.0; + } +} + #[derive(Debug, Clone)] -pub struct Controllers {} +pub struct Controllers { + strobe: bool, + primary: ControllerShiftReg, + secondary: ControllerShiftReg, +} impl Controllers { pub fn init() -> Self { - Self {} + Self { + strobe: false, + primary: ControllerShiftReg::new(), + secondary: ControllerShiftReg::new(), + } } + + pub fn reset(&mut self) { + *self = Self::init(); + } + pub fn read_joy1(&mut self) -> u8 { - 0 + if self.strobe { + self.primary.load(); + } + // TODO: Technically some kind of open-bus for all but the lowest 3 bits + self.primary.read() | 0x40 } + pub fn read_joy2(&mut self) -> u8 { - 0 + if self.strobe { + self.secondary.load(); + } + self.secondary.read() | 0x40 + } + + pub fn write_joy_strobe(&mut self, val: u8) { + if val & 1 == 0 { + if self.strobe { + self.primary.load(); + self.secondary.load(); + } + self.strobe = false; + } else { + self.strobe = true; + self.primary.load(); + self.secondary.load(); + } + } + + pub fn controller_1(&mut self) -> &mut ControllerState { + &mut self.primary.state + } + + pub fn controller_2(&mut self) -> &mut ControllerState { + &mut self.secondary.state } - pub fn write_joy_strobe(&mut self, val: u8) { } } diff --git a/src/cpu.rs b/src/cpu.rs index 7b67ad1..a47d04a 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1955,14 +1955,8 @@ pub enum DmaState { } impl DmaState { - pub fn merge(&mut self, other: DmaState) { - if matches!(other, DmaState::Running { .. }) { - if matches!(self, DmaState::Running { .. }) { - panic!("Merging incompatible dma states"); - } else { - *self = other; - } - } + pub fn reset(&mut self) { + *self = Self::Passive; } pub fn cycle(self, mem: &mut CpuMem<'_>, cpu: &mut Cpu) -> Option { @@ -1996,7 +1990,8 @@ impl DmaState { rem, read: None, } => { - if cpu.cycle % 2 != 1 { + if cpu.cycle % 2 == 0 { + writeln!(&mut cpu.debug_log, "DMA: Waiting for cycle").unwrap(); return Some(self); } let read = mem.read(cpu_addr); diff --git a/src/debugger.rs b/src/debugger.rs index b396c37..d1fb86a 100644 --- a/src/debugger.rs +++ b/src/debugger.rs @@ -39,8 +39,8 @@ pub struct DebuggerState { #[derive(Debug, Clone)] pub enum DebuggerMessage { - Run, - Pause, + // Run, + // Pause, SetPPUCycles(usize), RunPPUCycles, SetCPUCycles(usize), @@ -102,12 +102,6 @@ impl DebuggerState { pub fn view<'s>(&'s self, nes: &'s NES) -> Element<'s, DebuggerMessage> { column![ - row![ - button(image("./images/ic_fluent_play_24_filled.png")) - .on_press(DebuggerMessage::Run), - button(image("./images/ic_fluent_pause_24_filled.png")) - .on_press(DebuggerMessage::Pause), - ], iced::widget::rule::horizontal(2.), row![column![ text("Status"), @@ -142,7 +136,7 @@ impl DebuggerState { row![ column![ labelled("Cycle", text(nes.ppu().pixel)), - labelled("Scanline", text(nes.ppu().scanline)), + labelled("Scanline", text(if nes.ppu().scanline == 261 { -1 } else { nes.ppu().scanline as isize })), labelled("PPU Cycle", text(nes.ppu().cycle)), labelled("Frame", text(nes.ppu().frame_count)), labelled("V:", hex16(nes.ppu().background.v)), @@ -338,10 +332,10 @@ impl DebuggerState { }, |_, nes| nes.cpu.pc as usize == self.breakpoint, ), - DebuggerMessage::Run => { - Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted()) - } - DebuggerMessage::Pause => todo!(), + // DebuggerMessage::Run => { + // Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted()) + // } + // DebuggerMessage::Pause => todo!(), } } } diff --git a/src/hex_view.rs b/src/hex_view.rs index 63295c9..6e6854f 100644 --- a/src/hex_view.rs +++ b/src/hex_view.rs @@ -1,13 +1,36 @@ -use std::fmt; - -use iced::{ - Element, Font, - Length::Fill, - Task, - widget::{column, lazy, row, text}, +use std::{ + fmt::{self, Display}, + sync::Arc, }; -use crate::{mem::Mapped, PPU}; +use iced::{ + Element, Fill, Font, Padding, Task, + advanced::graphics::text::{ + FontSystem, + cosmic_text::{ + Attrs, Family, Metrics, + fontdb::{ID, Query}, + }, + font_system, + }, + keyboard::{Key, key::Named}, + mouse::{Button, ScrollDelta}, + widget::{column, lazy, row, text}, +}; +use iced_core::{ + Clipboard, Color, Event, Layout, Length, Pixels, Point, Rectangle, Shell, Size, Text, Vector, + Widget, + layout::{Limits, Node}, + mouse::{Cursor, Interaction}, + overlay, + renderer::{Quad, Style}, + widget::{ + Operation, Tree, + tree::{State, Tag}, + }, +}; + +use crate::{PPU, mem::Mapped, ppu::PPUMMRegisters}; pub trait Memory { fn peek(&self, val: usize) -> Option; @@ -16,7 +39,7 @@ pub trait Memory { } #[derive(Debug, Clone, Copy)] -pub struct Cpu<'a>(pub &'a Mapped); +struct Cpu<'a>(&'a Mapped); impl Memory for Cpu<'_> { fn peek(&self, val: usize) -> Option { @@ -33,15 +56,19 @@ impl Memory for Cpu<'_> { } #[derive(Debug, Clone, Copy)] -pub struct Ppu<'a>(pub &'a Mapped); +struct Ppu<'a>(&'a Mapped, &'a PPU); impl Memory for Ppu<'_> { fn peek(&self, val: usize) -> Option { - self.0.peek_ppu(val as u16) + match self.0.peek_ppu(val as u16) { + Ok(v) => Some(v), + Err(Some((PPUMMRegisters::Palette, off))) => Some(self.1.palette.ram(off as u8)), + Err(None) => None, + } } fn len(&self) -> usize { - 0x10000 + 0x4000 } fn edit_ver(&self) -> usize { @@ -101,38 +128,477 @@ impl HexView { } column![ text!("Hex view"), - iced::widget::scrollable(lazy( - mem.edit_ver(), - move |_| 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..mem.len()).step_by(16).map(|off| { - text!(" {off:04X} | {}", Row(off, mem)) - .font(Font::MONOSPACE) - .into() - })) - ) - )) + iced::widget::scrollable(lazy(mem.edit_ver(), move |_| 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..mem.len()).step_by(16).map(|off| { + text!(" {off:04X} | {}", Row(off, mem)) + .font(Font::MONOSPACE) + .into() + })) + ))) .width(Fill), ] .width(Fill) .into() } pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> { - self.render_any(Cpu(mem)) + // self.render_any(Cpu(mem)) + Element::new( + hex_editor::, HexEvent, iced::Renderer>(Cpu(mem)).font(Font::MONOSPACE), + ) } - pub fn render_ppu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> { - self.render_any(Ppu(mem)) + pub fn render_ppu<'a>(&self, mem: &'a Mapped, ppu: &'a PPU) -> Element<'a, HexEvent> { + // self.render_any(Ppu(mem, ppu)) + Element::new( + hex_editor::, HexEvent, iced::Renderer>(Ppu(mem, ppu)).font(Font::MONOSPACE), + ) } pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> { - self.render_any(Oam(ppu)) + Element::new( + hex_editor::, HexEvent, iced::Renderer>(Oam(ppu)).font(Font::MONOSPACE), + ) } pub fn update(&mut self, ev: HexEvent) -> Task { todo!() } } + +pub trait Buffer { + fn peek(&self, val: usize) -> Option; + fn len(&self) -> usize; +} + +impl Buffer for &[u8] { + fn peek(&self, val: usize) -> Option { + self.get(val).copied() + } + + fn len(&self) -> usize { + self.iter().len() + } +} +impl Buffer for M { + fn peek(&self, val: usize) -> Option { + self.peek(val) + } + + fn len(&self) -> usize { + self.len() + } +} + +pub fn hex_editor(raw: B) -> HexEditor { + HexEditor { + val: raw, + on_edit: None, + font: None, + font_size: None, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct HexEdit { + position: usize, + old_value: u8, + new_value: u8, +} + +pub struct HexEditor { + val: B, + font_size: Option, + font: Option, + on_edit: Option M>>, +} + +impl HexEditor +where + R: iced_core::text::Renderer, +{ + pub fn font(mut self, font: R::Font) -> Self { + self.font = Some(font); + self + } +} + +impl HexEditor +where + R: iced_core::text::Renderer, +{ + fn value_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle { + let size = self.font_size.unwrap_or(renderer.default_size()); + layout + .bounds() + .shrink(Padding::new(0.).top(size.0 * 1.5).right(10.)) + } + + fn scrollbar_bounds(&self, renderer: &R, layout: Layout<'_>) -> Rectangle { + let size = self.font_size.unwrap_or(renderer.default_size()); + layout.bounds().shrink( + Padding::new(0.) + .top(size.0 * 1.5) + .left(layout.bounds().width - 10.), + ) + } + + fn row_height(&self, renderer: &R) -> f32 { + self.font_size.unwrap_or(renderer.default_size()).0 * LINE_HEIGHT + } + + fn scroll_max(&self, renderer: &R, layout: Layout<'_>) -> f32 { + let size = self.font_size.unwrap_or(renderer.default_size()); + let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5)); + let rows = self.val.len().div_ceil(0x10); + (self.row_height(renderer) * rows as f32 - bounds.height).max(0.) + } + + fn scroll( + &self, + state: &mut HexEditorState, + renderer: &R, + layout: Layout<'_>, + delta: &ScrollDelta, + ) { + // let size = self.font_size.unwrap_or(renderer.default_size()); + // let bounds = layout.bounds().shrink(Padding::new(0.).top(size.0 * 1.5)); + // let rows = self.val.len().div_ceil(0x10); + let max = self.scroll_max(renderer, layout); + // println!("max: {max}, rows: {rows}"); + match delta { + ScrollDelta::Lines { y, .. } => { + state.offset_y += y * self.row_height(renderer); + } + ScrollDelta::Pixels { y, .. } => { + state.offset_y += y; + } + } + if state.offset_y > 0. { + state.offset_y = 0.; + } else if state.offset_y < -max { + state.offset_y = -max; + } + } +} + +#[derive(Default)] +struct HexEditorState { + offset_y: f32, + selected: usize, + dragging: bool, +} + +const LINE_HEIGHT: f32 = 1.3; + +impl Widget for HexEditor +where + R: iced_core::text::Renderer, +{ + fn tag(&self) -> Tag { + Tag::of::() + } + + fn state(&self) -> State { + State::new(HexEditorState::default()) + } + + fn size(&self) -> Size { + Size::new(Length::Fill, Length::Fill) + } + + fn layout(&mut self, _tree: &mut Tree, _renderer: &R, limits: &Limits) -> Node { + Node::new(limits.max()) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut R, + theme: &T, + style: &Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let state: &HexEditorState = tree.state.downcast_ref(); + let size = self.font_size.unwrap_or(renderer.default_size()); + let font = self.font.unwrap_or(renderer.default_font()); + + // let fonts = font_system(); + // let mut font_sys = fonts.write().unwrap(); + // let id = font_sys.raw().db().query(&Query { + // families: &[Family::Monospace], + // ..Query::default() + // }); + // let f = font_sys.raw().get_font(id.unwrap(), iced::advanced::graphics::text::cosmic_text::Weight(1)).unwrap(); + // let width = f.metrics(); + // println!("Width: {width:?}"); + // iced::advanced::graphics::text::cosmic_text::Buffer::new( + // font_sys.raw(), + // Metrics::new(size.0, size.0), + // ).layout_runs(); + // TODO: this needs to be computed from the font + let col_width = 0.6 * size.0; + + let rows = self.val.len().div_ceil(0x10); + let row_label_length = rows.ilog2().div_ceil(4) as usize; + let mut draw = |v: &str, pos: Point| { + renderer.fill_text( + Text { + content: format!("{}", v), + bounds: viewport.size(), + size, + line_height: size.into(), + font, + align_x: iced_core::text::Alignment::Left, + align_y: iced_core::alignment::Vertical::Top, + shaping: iced_core::text::Shaping::Basic, + wrapping: iced_core::text::Wrapping::None, + hint_factor: None, + }, + pos, + Color::WHITE, + layout.bounds(), + ); + }; + draw("0", Point::new(0., 0.)); + for i in 0..0x10 { + draw( + "0", + layout.position() + + Vector::new((3 + row_label_length + 3 * i) as f32 * col_width, 0.), + ); + draw( + "0", + layout.position() + + Vector::new((4 + row_label_length + 3 * i) as f32 * col_width, 0.), + ); + } + // renderer.fill_text( + // Text { + // content: format!( + // "{:width$} 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F {}", + // "", + // state.offset_y, + // width = row_label_length + // ), + // bounds: viewport.size(), + // size, + // line_height: size.into(), + // font, + // align_x: iced_core::text::Alignment::Left, + // align_y: iced_core::alignment::Vertical::Top, + // shaping: iced_core::text::Shaping::Basic, + // wrapping: iced_core::text::Wrapping::None, + // hint_factor: None, + // }, + // layout.position(), + // Color::WHITE, + // layout.bounds(), + // ); + struct HexV(Option); + impl Display for HexV { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(v) => write!(f, "{:02X}", v), + None => write!(f, "XX"), + } + } + } + // if rows > 0 { + // rows.ilog2() + // } + let bounds = self.value_bounds(renderer, layout); + let mut pos = bounds.position() + Vector::new(0., state.offset_y); + for row in 0..rows { + if bounds.contains(pos) || bounds.contains(pos + Vector::new(0., size.0 * LINE_HEIGHT)) + { + renderer.fill_text( + Text { + content: format!( + "{:0width$X}0: {} {} {} {} {} {} {} {} {} {} {} {} {} {} {} {}", + row, + HexV(self.val.peek(row * 0x10 + 0x0)), + HexV(self.val.peek(row * 0x10 + 0x1)), + HexV(self.val.peek(row * 0x10 + 0x2)), + HexV(self.val.peek(row * 0x10 + 0x3)), + HexV(self.val.peek(row * 0x10 + 0x4)), + HexV(self.val.peek(row * 0x10 + 0x5)), + HexV(self.val.peek(row * 0x10 + 0x6)), + HexV(self.val.peek(row * 0x10 + 0x7)), + HexV(self.val.peek(row * 0x10 + 0x8)), + HexV(self.val.peek(row * 0x10 + 0x9)), + HexV(self.val.peek(row * 0x10 + 0xA)), + HexV(self.val.peek(row * 0x10 + 0xB)), + HexV(self.val.peek(row * 0x10 + 0xC)), + HexV(self.val.peek(row * 0x10 + 0xD)), + HexV(self.val.peek(row * 0x10 + 0xE)), + HexV(self.val.peek(row * 0x10 + 0xF)), + width = row_label_length, + ), + bounds: viewport.size(), + size, + line_height: size.into(), + font, + align_x: iced_core::text::Alignment::Left, + align_y: iced_core::alignment::Vertical::Top, + shaping: iced_core::text::Shaping::Basic, + wrapping: iced_core::text::Wrapping::None, + hint_factor: None, + }, + pos, + Color::WHITE, + bounds, + ); + } + pos += Vector::new(0., size.0 * LINE_HEIGHT); + } + + let scrollbar = self.scrollbar_bounds(renderer, layout); + renderer.fill_quad( + Quad { + bounds: scrollbar, + ..Quad::default() + }, + Color::BLACK, + ); + let pos = state.offset_y / self.scroll_max(renderer, layout); + renderer.fill_quad( + Quad { + bounds: Rectangle::new( + Point::new(scrollbar.x, scrollbar.y - pos * (scrollbar.height - 20.)), + Size::new(10., 20.), + ), + ..Quad::default() + }, + Color::WHITE, + ); + } + + fn operate( + &mut self, + _tree: &mut Tree, + _layout: Layout<'_>, + _renderer: &R, + _operation: &mut dyn Operation, + ) { + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: Cursor, + renderer: &R, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, M>, + _viewport: &Rectangle, + ) { + // if !matches!(event, Event::Window(_)) { + // println!("Event: {:#?}", event); + // } + match event { + Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: Key::Named(Named::PageUp), + .. + }) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + self.scroll( + state, + renderer, + layout, + &ScrollDelta::Pixels { + x: 0., + y: self.value_bounds(renderer, layout).height, + }, + ); + shell.request_redraw(); + shell.capture_event(); + } + Event::Keyboard(iced::keyboard::Event::KeyPressed { + key: Key::Named(Named::PageDown), + .. + }) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + self.scroll( + state, + renderer, + layout, + &ScrollDelta::Pixels { + x: 0., + y: -self.value_bounds(renderer, layout).height, + }, + ); + shell.request_redraw(); + shell.capture_event(); + } + Event::Mouse(iced::mouse::Event::WheelScrolled { delta }) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + let bounds = self.value_bounds(renderer, layout); + if cursor.is_over(bounds) { + self.scroll(state, renderer, layout, delta); + shell.request_redraw(); + shell.capture_event(); + } + } + Event::Mouse(iced::mouse::Event::ButtonPressed(Button::Left)) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + let bounds = self.scrollbar_bounds(renderer, layout); + if let Some(pos) = cursor.position_in(bounds) { + state.offset_y = -(pos.y / bounds.height * self.scroll_max(renderer, layout)); + state.dragging = true; + shell.request_redraw(); + shell.capture_event(); + } + } + Event::Mouse(iced::mouse::Event::ButtonReleased(Button::Left)) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + state.dragging = false; + } + // Event::Mouse(iced::mouse::Event::CursorLeft) => { + // let state: &mut HexEditorState = tree.state.downcast_mut(); + // state.dragging = false; + // } + Event::Mouse(iced::mouse::Event::CursorMoved { .. }) => { + let state: &mut HexEditorState = tree.state.downcast_mut(); + if state.dragging { + let bounds = self.scrollbar_bounds(renderer, layout); + if let Some(pos) = cursor.position_in(bounds) { + state.offset_y = + -(pos.y / bounds.height * self.scroll_max(renderer, layout)); + shell.request_redraw(); + shell.capture_event(); + } + } + } + _ => (), + } + } + + fn mouse_interaction( + &self, + _tree: &Tree, + _layout: Layout<'_>, + _cursor: Cursor, + _viewport: &Rectangle, + _renderer: &R, + ) -> Interaction { + Interaction::None + } + + fn overlay<'a>( + &'a mut self, + _tree: &'a mut Tree, + _layout: Layout<'a>, + _renderer: &R, + _viewport: &Rectangle, + _translation: Vector, + ) -> Option> { + None + } +} diff --git a/src/lib.rs b/src/lib.rs index 0173e2d..1294272 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ use thiserror::Error; use crate::{ apu::APU, - controllers::Controllers, + controllers::{ControllerState, Controllers}, cpu::{CPUMMRegisters, ClockState, Cpu, CycleResult, DmaState}, debug::DebugLog, mem::{CpuMem, Mapped, PpuMem}, @@ -172,14 +172,14 @@ impl NES { br: &Break, ) -> bool { if let Some(dma) = self.dma.cycle( - &mut CpuMem::new( - &mut self.mapped, - &mut self.ppu, - &mut self.apu, - &mut self.dma, - &mut self.controller, - ), - &mut self.cpu + &mut CpuMem::new( + &mut self.mapped, + &mut self.ppu, + &mut self.apu, + &mut self.dma, + &mut self.controller, + ), + &mut self.cpu, ) { self.dma = dma; false @@ -211,22 +211,7 @@ impl NES { }; } let cpu_exec = if self.clock_count % 3 == 0 && self.clock_count > 0 { - let nmi = self.ppu.nmi_waiting() || self.apu.nmi_waiting(); - let irq = self.ppu.irq_waiting() || self.apu.irq_waiting(); - let mut dma = DmaState::Passive; - if self.run_cpu_cycle( - // &mut CpuMem::new( - // &mut self.mapped, - // &mut self.ppu, - // &mut self.apu, - // &mut dma, - // &mut self.controller, - // ), - // &mut self.dma, - // nmi, - // irq, - br, - ) { + if self.run_cpu_cycle(br) { println!("Returning early from clock_cycle"); return CycleResult { cpu_exec: true, @@ -235,7 +220,6 @@ impl NES { dbg_int: true, }; } - self.dma.merge(dma); self.cpu.cpu_cycle_update(); self.cpu.executed() } else if self.clock_count == 0 { @@ -268,6 +252,17 @@ impl NES { } pub fn reset(&mut self) { + self.clock_count = 0; + // dbg_int: false, + + // clock_count: 0, + // mapped, + // cpu: Cpu::init(), + // dma: DmaState::Passive, + // // ppu: PPU::with_chr_rom(chr_rom, mapper), + // ppu: PPU::init(), + // apu: APU::init(), + // controller: Controllers::init(), self.cpu.reset(&mut CpuMem::new( &mut self.mapped, &mut self.ppu, @@ -275,7 +270,10 @@ impl NES { &mut self.dma, &mut self.controller, )); + self.dma.reset(); self.ppu.reset(); + self.apu.reset(); + self.controller.reset(); } pub fn power_cycle(&mut self) { @@ -373,6 +371,13 @@ impl NES { pub fn cpu_cycle(&self) -> usize { self.cpu.cycle } + + pub fn controller_1(&mut self) -> &mut ControllerState { + self.controller.controller_1() + } + pub fn controller_2(&mut self) -> &mut ControllerState { + self.controller.controller_2() + } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] diff --git a/src/main.rs b/src/main.rs index 821ab5f..637af55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,31 @@ -use std::{collections::HashMap, fmt, time::Duration}; +use std::{ + collections::HashMap, + fmt, + time::{Duration, Instant}, +}; use iced::{ Color, Element, Length::{Fill, Shrink}, - Point, Renderer, Size, Subscription, Task, Theme, - advanced::graphics::compositor::Display, - mouse, + Point, Rectangle, Renderer, Size, Subscription, Task, Theme, + keyboard::{self, Key, Modifiers, key::Named}, + mouse, time, widget::{ - Canvas, + self, Button, Canvas, button, canvas::{Frame, Program}, - column, container, row, + column, container, image, row, }, window::{self, Id, Settings}, }; use nes_emu::{ - NES, + Break, NES, debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image}, header_menu::header_menu, hex_view::{HexEvent, HexView}, resize_watcher::resize_watcher, }; use tokio::{io::AsyncWriteExt, runtime::Runtime}; +use tracing::instrument::WithSubscriber; use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes"); @@ -28,10 +33,27 @@ use tracing_subscriber::EnvFilter; // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_red.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.nes"); // const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "int_nmi_exit_timing.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "render-updating.nes"); +// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_palette_shared.nes"); const ROM_FILE: &str = "./Super Mario Bros. (World).nes"; // const ROM_FILE: &str = "./cpu_timing_test.nes"; // const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.nes"; +/// Disable button in debug mode - used to disable play/pause buttons since the performance isn't fast enough +fn debug_disable<'a, Message, Theme, Renderer>( + btn: Button<'a, Message, Theme, Renderer>, +) -> Button<'a, Message, Theme, Renderer> +where + Theme: button::Catalog + 'a, + Renderer: iced::advanced::Renderer, +{ + if cfg!(debug_assertions) { + btn.on_press_maybe(None) + } else { + btn + } +} + extern crate nes_emu; fn main() -> Result<(), iced::Error> { @@ -101,10 +123,12 @@ impl fmt::Display for HeaderButton { } struct Emulator { + running: bool, nes: NES, windows: HashMap, debugger: DebuggerState, main_win_size: Size, + prev: [Instant; 2], } #[derive(Debug, Clone)] @@ -117,6 +141,9 @@ enum Message { // DebugInt, WindowClosed(Id), WindowOpened(Id), + Key(keyboard::Event), + Periodic(Instant), + SetRunning(bool), Header(HeaderButton), Hex(Id, HexEvent), Debugger(DebuggerMessage), @@ -142,20 +169,30 @@ impl Emulator { min_size: None, ..Settings::default() }); + let (win_2, task_2) = iced::window::open(Settings { + min_size: None, + ..Settings::default() + }); ( Self { nes, - windows: HashMap::from_iter([(win, WindowType::Main)]), + windows: HashMap::from_iter([ + (win, WindowType::Main), + (win_2, WindowType::Memory(MemoryTy::OAM, HexView {})) + ]), debugger: DebuggerState::new(), main_win_size: Size::new(0., 0.), + running: false, + prev: [Instant::now(); 2], }, - task.discard(), + Task::batch([task, task_2]).discard() + // task.discard(), ) } 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::Memory(ty, _)) => format!("NES {ty:?} Memory"), Some(WindowType::TileMap) => "NES TileMap".into(), Some(WindowType::TileViewer) => "NES Tile Viewer".into(), Some(WindowType::Palette) => "NES Palette Viewer".into(), @@ -266,6 +303,77 @@ impl Emulator { }) .discard(); } + Message::Key(key) => match key { + keyboard::Event::KeyPressed { + key: Key::Character(val), + modifiers: Modifiers::CTRL, + repeat: false, + .. + } => { + if val == "t" { + self.nes.reset(); + } + } + keyboard::Event::KeyPressed { + key, + modifiers: Modifiers::NONE, + repeat: false, + .. + } => { + if key == Key::Character("z".into()) { + self.nes.controller_1().set_a(true); + } else if key == Key::Character("x".into()) { + self.nes.controller_1().set_b(true); + } else if key == Key::Character("a".into()) { + self.nes.controller_1().set_select(true); + } else if key == Key::Character("s".into()) { + self.nes.controller_1().set_start(true); + } else if key == Key::Named(Named::ArrowDown) { + self.nes.controller_1().set_down(true); + } else if key == Key::Named(Named::ArrowUp) { + self.nes.controller_1().set_up(true); + } else if key == Key::Named(Named::ArrowLeft) { + self.nes.controller_1().set_left(true); + } else if key == Key::Named(Named::ArrowRight) { + self.nes.controller_1().set_right(true); + } + } + keyboard::Event::KeyReleased { + key, + modifiers: Modifiers::NONE, + .. + } => { + if key == Key::Character("z".into()) { + self.nes.controller_1().set_a(false); + } else if key == Key::Character("x".into()) { + self.nes.controller_1().set_b(false); + } else if key == Key::Character("a".into()) { + self.nes.controller_1().set_select(false); + } else if key == Key::Character("s".into()) { + self.nes.controller_1().set_start(false); + } else if key == Key::Named(Named::ArrowDown) { + self.nes.controller_1().set_down(false); + } else if key == Key::Named(Named::ArrowUp) { + self.nes.controller_1().set_up(false); + } else if key == Key::Named(Named::ArrowLeft) { + self.nes.controller_1().set_left(false); + } else if key == Key::Named(Named::ArrowRight) { + self.nes.controller_1().set_right(false); + } + } + _ => (), + }, + Message::Periodic(i) => { + if self.running { + // TODO: this should skip updating to avoid multiple frame skips + self.prev[1] = self.prev[0]; + self.prev[0] = i; + + self.nes.run_one_clock_cycle(&Break::default()); + while !self.nes.run_one_clock_cycle(&Break::default()).ppu_frame {} + } + } + Message::SetRunning(running) => self.running = running, } // self.image.0.clone_from(self.nes.image()); Task::none() @@ -275,10 +383,13 @@ impl Emulator { Subscription::batch([ window::close_events().map(Message::WindowClosed), window::open_events().map(Message::WindowOpened), + keyboard::listen().map(Message::Key), + time::every(Duration::from_millis(1000 / 60)).map(Message::Periodic), ]) } fn view(&self, win: Id) -> Element<'_, Message> { + // println!("Running view"); match self.windows.get(&win) { Some(WindowType::Main) => { let content = column![ @@ -300,7 +411,7 @@ impl Emulator { .render_cpu(self.nes.mem()) .map(move |e| Message::Hex(win, e)), MemoryTy::PPU => view - .render_ppu(self.nes.mem()) + .render_ppu(self.nes.mem(), self.nes.ppu()) .map(move |e| Message::Hex(win, e)), MemoryTy::OAM => view .render_oam(self.nes.ppu()) @@ -320,12 +431,22 @@ impl Emulator { Some(WindowType::Palette) => { dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into() } - Some(WindowType::Debugger) => { - container(self.debugger.view(&self.nes).map(Message::Debugger)) - .width(Fill) - .height(Fill) - .into() - } + Some(WindowType::Debugger) => column![ + row![ + debug_disable( + button(image("./images/ic_fluent_play_24_filled.png")) + .on_press(Message::SetRunning(true)) + ), + debug_disable( + button(image("./images/ic_fluent_pause_24_filled.png")) + .on_press(Message::SetRunning(false)) + ), + ], + self.debugger.view(&self.nes).map(Message::Debugger) + ] + .width(Fill) + .height(Fill) + .into(), None => panic!("Window not found"), // _ => todo!(), } @@ -364,6 +485,21 @@ impl Emulator { impl Program for Emulator { type State = (); + fn update( + &self, + _state: &mut Self::State, + _event: &iced::Event, + _bounds: Rectangle, + _cursor: iced::advanced::mouse::Cursor, + ) -> Option> { + // ~ 60 fps, I think? + // Some(widget::Action::request_redraw_at( + // Instant::now() + Duration::from_millis(10), + // )) + // Some(widget::Action::request_redraw()) + None + } + fn draw( &self, _state: &Self::State, @@ -372,6 +508,7 @@ impl Program for Emulator { bounds: iced::Rectangle, _cursor: mouse::Cursor, ) -> Vec> { + // let start = Instant::now(); // const SIZE: f32 = 2.; let mut frame = Frame::new( renderer, @@ -381,16 +518,28 @@ impl Program for Emulator { // }, ); 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), - ); - } - } + // TODO: use image for better? performance + frame.draw_image( + Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)), + widget::canvas::Image::new(widget::image::Handle::from_rgba( + 256, + 240, + self.nes.image().image(), + )) + .filter_method(image::FilterMethod::Nearest) + .snap(true), + ); + // 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), + // ); + // } + // } + // println!("Rendered frame in {}ms", start.elapsed().as_millis()); vec![frame.into_geometry()] } } diff --git a/src/mem.rs b/src/mem.rs index 6815408..b0b3482 100644 --- a/src/mem.rs +++ b/src/mem.rs @@ -6,13 +6,13 @@ use crate::{ }; #[derive(Debug, Clone)] -pub enum Value<'a, R> { +pub enum Value { Value(u8), - Register { reg: &'a R, offset: u16 }, + Register { reg: R, offset: u16 }, } -impl Value<'_, R> { - pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 { +impl Value { + 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), @@ -117,7 +117,7 @@ pub struct MemoryMap { } impl MemoryMap { - pub fn read(&self, addr: u16) -> Value<'_, R> { + pub fn read(&self, addr: u16) -> Value { // self.edit_ver += 1; for segment in &self.segments { if segment.position <= addr && addr - segment.position < segment.size { @@ -125,7 +125,7 @@ impl MemoryMap { 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, + reg: *reg, offset: addr - segment.position, }, Data::Mirror(pos) => { @@ -201,28 +201,28 @@ impl MemoryMap { None } - fn peek_val(&self, addr: u16) -> Option { + fn peek_val(&self, addr: u16) -> Result> { 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::RAM(items) => Ok(items[(addr - segment.position) as usize]), + Data::ROM(items) => Ok(items[(addr - segment.position) as usize]), + Data::Reg(r) => Err(Some((*r, addr - segment.position))), Data::Mirror(pos) => { let offset = addr - segment.position; self.peek_val(pos + offset) } - Data::Disabled => None, + Data::Disabled => Err(None), }; } } - None + Err(None) } } impl Memory for MemoryMap { fn peek(&self, addr: usize) -> Option { - self.peek_val(addr as u16) + self.peek_val(addr as u16).ok() } fn len(&self) -> usize { @@ -455,14 +455,14 @@ impl Mapped { } } - pub fn peek_ppu(&self, addr: u16) -> Option { + pub fn peek_ppu(&self, addr: u16) -> Result> { self.ppu.peek_val(addr) } pub fn ppu_edit_ver(&self) -> usize { self.ppu.edit_ver } pub fn peek_cpu(&self, addr: u16) -> Option { - self.cpu.peek_val(addr) + self.cpu.peek_val(addr).ok() } pub fn cpu_edit_ver(&self) -> usize { self.cpu.edit_ver @@ -638,7 +638,7 @@ impl<'a> PpuMem<'a> { pub fn new(mem: &'a mut Mapped) -> Self { Self(mem) } - pub fn read(&mut self, addr: u16) -> Value<'_, PPUMMRegisters> { + pub fn read(&mut self, addr: u16) -> Value { self.0.ppu.read(addr) } pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&PPUMMRegisters, u16, u8)) { diff --git a/src/ppu.rs b/src/ppu.rs index bbb04c6..3b35e9d 100644 --- a/src/ppu.rs +++ b/src/ppu.rs @@ -1,15 +1,13 @@ use std::fmt; +use bytes::{Bytes, BytesMut}; use iced::{ Point, Size, advanced::graphics::geometry::Renderer, widget::canvas::{Fill, Frame}, }; -use crate::{ - hex_view::Memory, - mem::{Mapped, PpuMem}, -}; +use crate::mem::{Mapped, PpuMem, Value}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Color { @@ -33,18 +31,25 @@ impl fmt::Display for Color { #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RenderBuffer { buffer: Box<[Color]>, + raw_rgba: BytesMut, } impl RenderBuffer { pub fn empty() -> Self { Self { buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(), + raw_rgba: BytesMut::from_iter(vec![0; W * H * 4]), } } pub fn write(&mut self, line: usize, pixel: usize, color: Color) { assert!(line < H && pixel < W); self.buffer[line * W + pixel] = color; + let pos = (line * W + pixel) * 4; + self.raw_rgba[pos] = color.r; + self.raw_rgba[pos + 1] = color.g; + self.raw_rgba[pos + 2] = color.b; + self.raw_rgba[pos + 3] = 0xFF; } pub fn read(&self, line: usize, pixel: usize) -> Color { assert!(line < H && pixel < W); @@ -53,6 +58,12 @@ impl RenderBuffer { pub fn clone_from(&mut self, other: &Self) { self.buffer.copy_from_slice(&other.buffer); + // self.raw_rgba.fr + self.raw_rgba.copy_from_slice(&other.raw_rgba); + } + + pub fn image(&self) -> Bytes { + Bytes::copy_from_slice(&self.raw_rgba) } pub fn assert_eq(&self, other: &Self) { @@ -72,7 +83,7 @@ impl RenderBuffer { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum PPUMMRegisters { Palette, } @@ -84,18 +95,6 @@ pub struct OAM { edit_ver: usize, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum BackgroundState { - NameTableBytePre, - NameTableByte, - AttrTableBytePre, - AttrTableByte, - PatternTableTileLowPre, - PatternTableTileLow, - PatternTableTileHighPre, - PatternTableTileHigh, -} - #[derive(Debug, Clone)] pub struct Background { /// Current vram address, 15 bits @@ -112,8 +111,6 @@ pub struct Background { pub vram_column: bool, pub second_pattern: bool, - state: BackgroundState, - pub cur_nametable: u8, pub cur_attr: u8, pub next_attr: u8, @@ -470,6 +467,10 @@ impl Palette { debug_assert!(idx < 0x20, "Palette index out of range"); self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize] } + + pub fn ram(&self, offset: u8) -> u8 { + self.ram[offset as usize] + } } #[derive(Clone)] @@ -482,10 +483,12 @@ pub struct PPU { pub scanline: usize, pub pixel: usize, + vram_buffer: u8, + pub mask: Mask, pub vblank: bool, - palette: Palette, + pub palette: Palette, pub background: Background, oam: OAM, pub render_buffer: RenderBuffer<256, 240>, @@ -542,6 +545,7 @@ impl PPU { frame_count: 0, nmi_enabled: false, // nmi_waiting: false, + // TODO: Is even in the right initial state? even: false, scanline: 0, pixel: 25, @@ -553,7 +557,6 @@ impl PPU { w: false, vram_column: false, second_pattern: false, - state: BackgroundState::NameTableBytePre, cur_high: 0, cur_low: 0, cur_shift_high: 0, @@ -568,6 +571,7 @@ impl PPU { addr: 0, edit_ver: 0, }, + vram_buffer: 0, } } pub fn reset(&mut self) { @@ -593,10 +597,22 @@ impl PPU { 5 => panic!("ppuscroll is write-only"), 6 => panic!("ppuaddr is write-only"), 7 => { + // TODO: read buffer only applies to ppu data, not palette ram... // println!("Updating v for ppudata read"); - let val = mem.read(self.background.v).reg_map(|a, off| match a { - PPUMMRegisters::Palette => self.palette.ram[off as usize], - }); + // self.vram_buffer = + let val = match mem.read(self.background.v) { + Value::Value(v) => { + let val = self.vram_buffer; + self.vram_buffer = v; + val + }, + Value::Register { reg: PPUMMRegisters::Palette, offset } => { + self.palette.ram[offset as usize] + }, + }; + // .reg_map(|a, off| match a { + // PPUMMRegisters::Palette => self.palette.ram[off as usize], + // }); // if self.background self.increment_v(); val @@ -656,7 +672,7 @@ impl PPU { } } 0x06 => { - // TODO: ppu addr + // TODO: this actually sets T, which is copied to v later (~ a pixel later?) if self.background.w { self.background.v = u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]); @@ -672,10 +688,16 @@ impl PPU { } 0x07 => { // println!("Writing: {:02X}, @{:04X}", val, self.background.v); - mem.write(self.background.v, val, |r, o, v| match r { + mem.write(self.background.v, val, |r, mut o, v| match r { PPUMMRegisters::Palette => { // println!("Writing {:02X} to {:02X}", v & 0x3F, o); - self.palette.ram[o as usize] = v & 0x3F; + if o % 4 == 0 { + o = o & !0b1_0000; + self.palette.ram[o as usize] = v & 0x3F; + self.palette.ram[(o | 0b1_0000) as usize] = v & 0x3F; + } else { + self.palette.ram[o as usize] = v & 0x3F; + } } }); self.increment_v(); @@ -710,6 +732,7 @@ impl PPU { self.scanline = 0; self.pixel = 0; self.even = !self.even; + self.frame_count += 1; } if self.pixel == 341 { self.pixel = 0; @@ -849,8 +872,6 @@ impl PPU { } if self.scanline == 241 && self.pixel == 1 { self.vblank = true; - self.frame_count += 1; - self.background.state = BackgroundState::NameTableBytePre; return true; } return false; diff --git a/src/test_roms/even_odd.s b/src/test_roms/even_odd.s index 7cad08f..6db7156 100644 --- a/src/test_roms/even_odd.s +++ b/src/test_roms/even_odd.s @@ -11,7 +11,7 @@ reset: stx $2000 ; Disable NMI (by writing zero) stx $4010 ; Disable DMC IRQs ldx #$08 - stx $2001 ; Disable rendering + stx $2001 ; Enable rendering bit $2002 ; Clear vblank flag by reading ppu status VBLANKWAIT1: diff --git a/src/test_roms/ppu.rs b/src/test_roms/ppu.rs index f1b2b71..ef7ecd0 100644 --- a/src/test_roms/ppu.rs +++ b/src/test_roms/ppu.rs @@ -83,8 +83,14 @@ rom_test!(ppu_fill_name_table, "ppu_fill_name_table.nes", |nes| { rom_test!(ppu_vertical_write, "ppu_vertical_write.nes", |nes| { assert_eq!(nes.last_instruction(), "0x8040 HLT :2 []"); - assert_eq!(nes.mem().peek_ppu(0x2000), Some(1)); - assert_eq!(nes.mem().peek_ppu(0x2020), Some(1)); - assert_eq!(nes.mem().peek_ppu(0x2400), Some(2)); - assert_eq!(nes.mem().peek_ppu(0x2401), Some(2)); + assert_eq!(nes.mem().peek_ppu(0x2000), Ok(1)); + assert_eq!(nes.mem().peek_ppu(0x2020), Ok(1)); + assert_eq!(nes.mem().peek_ppu(0x2400), Ok(2)); + assert_eq!(nes.mem().peek_ppu(0x2401), Ok(2)); +}); + +rom_test!(ppu_palette_shared, "ppu_palette_shared.nes", |nes| { + assert_eq!(nes.last_instruction(), "0x8078 HLT :2 []"); + + }); diff --git a/src/test_roms/ppu_palette_shared.s b/src/test_roms/ppu_palette_shared.s new file mode 100644 index 0000000..da63fca --- /dev/null +++ b/src/test_roms/ppu_palette_shared.s @@ -0,0 +1,70 @@ +.include "testing.s" + +reset: + sei ; Ignore IRQs while starting up + cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) + ldx #$40 + stx $4017 ; Disable APU frame IRQ + ldx #$ff + txs ; Set stack pointer to 0x1ff + inx ; Set x to zero + stx $2000 ; Disable NMI (by writing zero) + stx $4010 ; Disable DMC IRQs + ldx #$08 + stx $2001 ; Disable rendering + + bit $2002 ; Clear vblank flag by reading ppu status +VBLANKWAIT1: + bit $2002 + bpl VBLANKWAIT1 +VBLANKWAIT2: + bit $2002 + bpl VBLANKWAIT2 + + lda #$3F + sta PPUADDR + lda #$00 + sta PPUADDR + lda #$01 + ldx #$10 +fill_loop: + sta PPUDATA + dex + bne fill_loop + + lda #$3F + sta PPUADDR + lda #$10 + sta PPUADDR + + lda PPUDATA + cmp #$01 + bne fail + lda PPUDATA + lda PPUDATA + lda PPUDATA + lda PPUDATA + cmp #$01 + bne fail + lda PPUDATA + lda PPUDATA + lda PPUDATA + lda PPUDATA + cmp #$01 + bne fail + lda PPUDATA + lda PPUDATA + lda PPUDATA + lda PPUDATA + cmp #$01 + bne fail + stp + jmp $8000 +fail: + stp + +nmi: + stp + +irq: + stp diff --git a/src/test_roms/render-updating.s b/src/test_roms/render-updating.s new file mode 100644 index 0000000..6dedcb2 --- /dev/null +++ b/src/test_roms/render-updating.s @@ -0,0 +1,96 @@ +.include "testing.s" + +.macro chr b,s +.repeat s +.byte b +.endrepeat +.endmacro + +.pushseg +.segment "CHARS" +chr $FF,16 ; Full +chr $00,8 +chr $FF,8 ; Gray +chr $55,16 ; Vertical stripes +chr $AA,16 ; Vertical stripes - reverse +.repeat 8 +.byte $FF +.byte $00 +.endrepeat ; Horizontal stripes +.repeat 8 +.byte $00 +.byte $FF +.endrepeat ; Horizontal stripes - reverse +.repeat 8 +.byte $55 +.byte $AA +.endrepeat ; Checkerboard +.repeat 8 +.byte $AA +.byte $55 +.endrepeat ; Checkerboard - reverse +.popseg + +zp_res CUR +zp_res POS + +reset: + sei ; Ignore IRQs while starting up + cld ; disabled decimal mode (iirc it doesn't work properly on NES anyway) + ldx #$40 + stx $4017 ; Disable APU frame IRQ + ldx #$ff + txs ; Set stack pointer to 0x1ff + inx ; Set x to zero + stx $2000 ; Disable NMI (by writing zero) + stx $4010 ; Disable DMC IRQs + ldx #$0A + stx $2001 ; Disable rendering + + bit $2002 ; Clear vblank flag by reading ppu status +VBLANKWAIT1: + bit $2002 + bpl VBLANKWAIT1 +VBLANKWAIT2: + bit $2002 + bpl VBLANKWAIT2 + lda #$1 + sta CUR + lda #$0 + sta POS + lda #$80 + sta PPUCTRL +loop: + jmp loop + +nmi: + lda #$20 + sta PPUADDR ; MSB + lda POS + adc #$1 + sta POS + sta PPUADDR + bne update + clc + lda CUR + adc #$1 + sta CUR + cmp #$7 + bne update + lda #$0 + sta CUR +update: + lda CUR + sta PPUDATA + + lda #$0 + sta PPUSCROLL + sta PPUSCROLL + + lda #$80 + sta PPUCTRL + + rti + +irq: + stp