2026-02-07 update
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s
Some checks failed
Cargo Build & Test / Rust project - latest (stable) (push) Failing after 8s
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2196,7 +2196,9 @@ name = "nes-emu"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitfield",
|
"bitfield",
|
||||||
|
"bytes",
|
||||||
"iced",
|
"iced",
|
||||||
|
"iced_core",
|
||||||
"rfd",
|
"rfd",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ edition = "2024"
|
|||||||
bitfield = "0.19.3"
|
bitfield = "0.19.3"
|
||||||
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
|
# iced = { version = "0.14.0", features = ["debug", "canvas", "tokio", "lazy", "image", "advanced"] }
|
||||||
iced = { path = "../iced", 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"
|
rfd = "0.17.2"
|
||||||
# iced_graphics = { version = "0.14.0", features = ["geometry", "image"] }
|
# iced_graphics = { version = "0.14.0", features = ["geometry", "image"] }
|
||||||
# iced_widget = { version = "0.13.4", features = ["canvas", "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"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
|
tracing-subscriber = { version = "0.3.20", features = ["ansi", "chrono", "env-filter", "json", "serde"] }
|
||||||
|
bytes = "*"
|
||||||
|
|||||||
11
src/apu.rs
11
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 {
|
pub fn read_reg(&mut self, offset: u16) -> u8 {
|
||||||
// println!("APU read: {offset:02X}");
|
// println!("APU read: {offset:02X}");
|
||||||
match offset {
|
match offset {
|
||||||
@@ -177,6 +182,7 @@ impl APU {
|
|||||||
_ => panic!("No register at {:X}", offset),
|
_ => panic!("No register at {:X}", offset),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
||||||
// println!("APU write: {offset:02X} <= {val:02X}");
|
// println!("APU write: {offset:02X} <= {val:02X}");
|
||||||
match offset {
|
match offset {
|
||||||
@@ -238,6 +244,7 @@ impl APU {
|
|||||||
self.pulse_1.q_frame_clock();
|
self.pulse_1.q_frame_clock();
|
||||||
self.pulse_2.q_frame_clock(); // TODO: clock all
|
self.pulse_2.q_frame_clock(); // TODO: clock all
|
||||||
}
|
}
|
||||||
|
|
||||||
fn h_frame_clock(&mut self) {
|
fn h_frame_clock(&mut self) {
|
||||||
self.pulse_1.q_frame_clock();
|
self.pulse_1.q_frame_clock();
|
||||||
self.pulse_1.h_frame_clock();
|
self.pulse_1.h_frame_clock();
|
||||||
@@ -271,15 +278,19 @@ impl APU {
|
|||||||
}
|
}
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_nmi(&self) -> bool {
|
pub fn peek_nmi(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nmi_waiting(&mut self) -> bool {
|
pub fn nmi_waiting(&mut self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_irq(&self) -> bool {
|
pub fn peek_irq(&self) -> bool {
|
||||||
self.frame_counter.irq
|
self.frame_counter.irq
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irq_waiting(&mut self) -> bool {
|
pub fn irq_waiting(&mut self) -> bool {
|
||||||
self.frame_counter.irq
|
self.frame_counter.irq
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Controllers {}
|
pub struct Controllers {
|
||||||
|
strobe: bool,
|
||||||
|
primary: ControllerShiftReg,
|
||||||
|
secondary: ControllerShiftReg,
|
||||||
|
}
|
||||||
|
|
||||||
impl Controllers {
|
impl Controllers {
|
||||||
pub fn init() -> Self {
|
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 {
|
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 {
|
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) { }
|
|
||||||
}
|
}
|
||||||
|
|||||||
13
src/cpu.rs
13
src/cpu.rs
@@ -1955,14 +1955,8 @@ pub enum DmaState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DmaState {
|
impl DmaState {
|
||||||
pub fn merge(&mut self, other: DmaState) {
|
pub fn reset(&mut self) {
|
||||||
if matches!(other, DmaState::Running { .. }) {
|
*self = Self::Passive;
|
||||||
if matches!(self, DmaState::Running { .. }) {
|
|
||||||
panic!("Merging incompatible dma states");
|
|
||||||
} else {
|
|
||||||
*self = other;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cycle(self, mem: &mut CpuMem<'_>, cpu: &mut Cpu) -> Option<Self> {
|
pub fn cycle(self, mem: &mut CpuMem<'_>, cpu: &mut Cpu) -> Option<Self> {
|
||||||
@@ -1996,7 +1990,8 @@ impl DmaState {
|
|||||||
rem,
|
rem,
|
||||||
read: None,
|
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);
|
return Some(self);
|
||||||
}
|
}
|
||||||
let read = mem.read(cpu_addr);
|
let read = mem.read(cpu_addr);
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ pub struct DebuggerState {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DebuggerMessage {
|
pub enum DebuggerMessage {
|
||||||
Run,
|
// Run,
|
||||||
Pause,
|
// Pause,
|
||||||
SetPPUCycles(usize),
|
SetPPUCycles(usize),
|
||||||
RunPPUCycles,
|
RunPPUCycles,
|
||||||
SetCPUCycles(usize),
|
SetCPUCycles(usize),
|
||||||
@@ -102,12 +102,6 @@ impl DebuggerState {
|
|||||||
|
|
||||||
pub fn view<'s>(&'s self, nes: &'s NES) -> Element<'s, DebuggerMessage> {
|
pub fn view<'s>(&'s self, nes: &'s NES) -> Element<'s, DebuggerMessage> {
|
||||||
column![
|
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.),
|
iced::widget::rule::horizontal(2.),
|
||||||
row![column![
|
row![column![
|
||||||
text("Status"),
|
text("Status"),
|
||||||
@@ -142,7 +136,7 @@ impl DebuggerState {
|
|||||||
row![
|
row![
|
||||||
column![
|
column![
|
||||||
labelled("Cycle", text(nes.ppu().pixel)),
|
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("PPU Cycle", text(nes.ppu().cycle)),
|
||||||
labelled("Frame", text(nes.ppu().frame_count)),
|
labelled("Frame", text(nes.ppu().frame_count)),
|
||||||
labelled("V:", hex16(nes.ppu().background.v)),
|
labelled("V:", hex16(nes.ppu().background.v)),
|
||||||
@@ -338,10 +332,10 @@ impl DebuggerState {
|
|||||||
},
|
},
|
||||||
|_, nes| nes.cpu.pc as usize == self.breakpoint,
|
|_, nes| nes.cpu.pc as usize == self.breakpoint,
|
||||||
),
|
),
|
||||||
DebuggerMessage::Run => {
|
// DebuggerMessage::Run => {
|
||||||
Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
|
// Self::run_until(nes, &Break { ..Break::default() }, |_, nes| nes.halted())
|
||||||
}
|
// }
|
||||||
DebuggerMessage::Pause => todo!(),
|
// DebuggerMessage::Pause => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
508
src/hex_view.rs
508
src/hex_view.rs
@@ -1,13 +1,36 @@
|
|||||||
use std::fmt;
|
use std::{
|
||||||
|
fmt::{self, Display},
|
||||||
use iced::{
|
sync::Arc,
|
||||||
Element, Font,
|
|
||||||
Length::Fill,
|
|
||||||
Task,
|
|
||||||
widget::{column, lazy, row, text},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 {
|
pub trait Memory {
|
||||||
fn peek(&self, val: usize) -> Option<u8>;
|
fn peek(&self, val: usize) -> Option<u8>;
|
||||||
@@ -16,7 +39,7 @@ pub trait Memory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Cpu<'a>(pub &'a Mapped);
|
struct Cpu<'a>(&'a Mapped);
|
||||||
|
|
||||||
impl Memory for Cpu<'_> {
|
impl Memory for Cpu<'_> {
|
||||||
fn peek(&self, val: usize) -> Option<u8> {
|
fn peek(&self, val: usize) -> Option<u8> {
|
||||||
@@ -33,15 +56,19 @@ impl Memory for Cpu<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct Ppu<'a>(pub &'a Mapped);
|
struct Ppu<'a>(&'a Mapped, &'a PPU);
|
||||||
|
|
||||||
impl Memory for Ppu<'_> {
|
impl Memory for Ppu<'_> {
|
||||||
fn peek(&self, val: usize) -> Option<u8> {
|
fn peek(&self, val: usize) -> Option<u8> {
|
||||||
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 {
|
fn len(&self) -> usize {
|
||||||
0x10000
|
0x4000
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit_ver(&self) -> usize {
|
fn edit_ver(&self) -> usize {
|
||||||
@@ -101,9 +128,7 @@ impl HexView {
|
|||||||
}
|
}
|
||||||
column![
|
column![
|
||||||
text!("Hex view"),
|
text!("Hex view"),
|
||||||
iced::widget::scrollable(lazy(
|
iced::widget::scrollable(lazy(mem.edit_ver(), move |_| column(
|
||||||
mem.edit_ver(),
|
|
||||||
move |_| column(
|
|
||||||
[
|
[
|
||||||
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
|
text!(" | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F")
|
||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
@@ -115,24 +140,465 @@ impl HexView {
|
|||||||
.font(Font::MONOSPACE)
|
.font(Font::MONOSPACE)
|
||||||
.into()
|
.into()
|
||||||
}))
|
}))
|
||||||
)
|
)))
|
||||||
))
|
|
||||||
.width(Fill),
|
.width(Fill),
|
||||||
]
|
]
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
pub fn render_cpu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
|
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::<Cpu<'a>, HexEvent, iced::Renderer>(Cpu(mem)).font(Font::MONOSPACE),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
pub fn render_ppu<'a>(&self, mem: &'a Mapped) -> Element<'a, HexEvent> {
|
pub fn render_ppu<'a>(&self, mem: &'a Mapped, ppu: &'a PPU) -> Element<'a, HexEvent> {
|
||||||
self.render_any(Ppu(mem))
|
// self.render_any(Ppu(mem, ppu))
|
||||||
|
Element::new(
|
||||||
|
hex_editor::<Ppu<'a>, HexEvent, iced::Renderer>(Ppu(mem, ppu)).font(Font::MONOSPACE),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> {
|
pub fn render_oam<'a>(&self, ppu: &'a PPU) -> Element<'a, HexEvent> {
|
||||||
self.render_any(Oam(ppu))
|
Element::new(
|
||||||
|
hex_editor::<Oam<'a>, HexEvent, iced::Renderer>(Oam(ppu)).font(Font::MONOSPACE),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
|
pub fn update(&mut self, ev: HexEvent) -> Task<HexEvent> {
|
||||||
todo!()
|
todo!()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait Buffer {
|
||||||
|
fn peek(&self, val: usize) -> Option<u8>;
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Buffer for &[u8] {
|
||||||
|
fn peek(&self, val: usize) -> Option<u8> {
|
||||||
|
self.get(val).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.iter().len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<M: Memory> Buffer for M {
|
||||||
|
fn peek(&self, val: usize) -> Option<u8> {
|
||||||
|
self.peek(val)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
self.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hex_editor<B: Buffer, M, R: iced_core::text::Renderer>(raw: B) -> HexEditor<B, M, R> {
|
||||||
|
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<B, M, R: iced_core::text::Renderer> {
|
||||||
|
val: B,
|
||||||
|
font_size: Option<Pixels>,
|
||||||
|
font: Option<R::Font>,
|
||||||
|
on_edit: Option<Box<dyn Fn(HexEdit) -> M>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B, M, R> HexEditor<B, M, R>
|
||||||
|
where
|
||||||
|
R: iced_core::text::Renderer,
|
||||||
|
{
|
||||||
|
pub fn font(mut self, font: R::Font) -> Self {
|
||||||
|
self.font = Some(font);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<B: Buffer, M, R> HexEditor<B, M, R>
|
||||||
|
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<B: Buffer, M, T, R> Widget<M, T, R> for HexEditor<B, M, R>
|
||||||
|
where
|
||||||
|
R: iced_core::text::Renderer,
|
||||||
|
{
|
||||||
|
fn tag(&self) -> Tag {
|
||||||
|
Tag::of::<HexEditorState>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn state(&self) -> State {
|
||||||
|
State::new(HexEditorState::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(&self) -> Size<Length> {
|
||||||
|
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<u8>);
|
||||||
|
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<overlay::Element<'a, M, T, R>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
43
src/lib.rs
43
src/lib.rs
@@ -20,7 +20,7 @@ use thiserror::Error;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apu::APU,
|
apu::APU,
|
||||||
controllers::Controllers,
|
controllers::{ControllerState, Controllers},
|
||||||
cpu::{CPUMMRegisters, ClockState, Cpu, CycleResult, DmaState},
|
cpu::{CPUMMRegisters, ClockState, Cpu, CycleResult, DmaState},
|
||||||
debug::DebugLog,
|
debug::DebugLog,
|
||||||
mem::{CpuMem, Mapped, PpuMem},
|
mem::{CpuMem, Mapped, PpuMem},
|
||||||
@@ -179,7 +179,7 @@ impl NES {
|
|||||||
&mut self.dma,
|
&mut self.dma,
|
||||||
&mut self.controller,
|
&mut self.controller,
|
||||||
),
|
),
|
||||||
&mut self.cpu
|
&mut self.cpu,
|
||||||
) {
|
) {
|
||||||
self.dma = dma;
|
self.dma = dma;
|
||||||
false
|
false
|
||||||
@@ -211,22 +211,7 @@ impl NES {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
let cpu_exec = if self.clock_count % 3 == 0 && self.clock_count > 0 {
|
let cpu_exec = if self.clock_count % 3 == 0 && self.clock_count > 0 {
|
||||||
let nmi = self.ppu.nmi_waiting() || self.apu.nmi_waiting();
|
if self.run_cpu_cycle(br) {
|
||||||
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,
|
|
||||||
) {
|
|
||||||
println!("Returning early from clock_cycle");
|
println!("Returning early from clock_cycle");
|
||||||
return CycleResult {
|
return CycleResult {
|
||||||
cpu_exec: true,
|
cpu_exec: true,
|
||||||
@@ -235,7 +220,6 @@ impl NES {
|
|||||||
dbg_int: true,
|
dbg_int: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
self.dma.merge(dma);
|
|
||||||
self.cpu.cpu_cycle_update();
|
self.cpu.cpu_cycle_update();
|
||||||
self.cpu.executed()
|
self.cpu.executed()
|
||||||
} else if self.clock_count == 0 {
|
} else if self.clock_count == 0 {
|
||||||
@@ -268,6 +252,17 @@ impl NES {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
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(
|
self.cpu.reset(&mut CpuMem::new(
|
||||||
&mut self.mapped,
|
&mut self.mapped,
|
||||||
&mut self.ppu,
|
&mut self.ppu,
|
||||||
@@ -275,7 +270,10 @@ impl NES {
|
|||||||
&mut self.dma,
|
&mut self.dma,
|
||||||
&mut self.controller,
|
&mut self.controller,
|
||||||
));
|
));
|
||||||
|
self.dma.reset();
|
||||||
self.ppu.reset();
|
self.ppu.reset();
|
||||||
|
self.apu.reset();
|
||||||
|
self.controller.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn power_cycle(&mut self) {
|
pub fn power_cycle(&mut self) {
|
||||||
@@ -373,6 +371,13 @@ impl NES {
|
|||||||
pub fn cpu_cycle(&self) -> usize {
|
pub fn cpu_cycle(&self) -> usize {
|
||||||
self.cpu.cycle
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
|
||||||
|
|||||||
197
src/main.rs
197
src/main.rs
@@ -1,26 +1,31 @@
|
|||||||
use std::{collections::HashMap, fmt, time::Duration};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
fmt,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
use iced::{
|
use iced::{
|
||||||
Color, Element,
|
Color, Element,
|
||||||
Length::{Fill, Shrink},
|
Length::{Fill, Shrink},
|
||||||
Point, Renderer, Size, Subscription, Task, Theme,
|
Point, Rectangle, Renderer, Size, Subscription, Task, Theme,
|
||||||
advanced::graphics::compositor::Display,
|
keyboard::{self, Key, Modifiers, key::Named},
|
||||||
mouse,
|
mouse, time,
|
||||||
widget::{
|
widget::{
|
||||||
Canvas,
|
self, Button, Canvas, button,
|
||||||
canvas::{Frame, Program},
|
canvas::{Frame, Program},
|
||||||
column, container, row,
|
column, container, image, row,
|
||||||
},
|
},
|
||||||
window::{self, Id, Settings},
|
window::{self, Id, Settings},
|
||||||
};
|
};
|
||||||
use nes_emu::{
|
use nes_emu::{
|
||||||
NES,
|
Break, NES,
|
||||||
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
|
debugger::{DbgImage, DebuggerMessage, DebuggerState, dbg_image},
|
||||||
header_menu::header_menu,
|
header_menu::header_menu,
|
||||||
hex_view::{HexEvent, HexView},
|
hex_view::{HexEvent, HexView},
|
||||||
resize_watcher::resize_watcher,
|
resize_watcher::resize_watcher,
|
||||||
};
|
};
|
||||||
use tokio::{io::AsyncWriteExt, runtime::Runtime};
|
use tokio::{io::AsyncWriteExt, runtime::Runtime};
|
||||||
|
use tracing::instrument::WithSubscriber;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "even_odd.nes");
|
// 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_red.nes");
|
||||||
// const ROM_FILE: &str = concat!(env!("ROM_DIR"), "/", "ppu_fill_name_table.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"), "/", "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 = "./Super Mario Bros. (World).nes";
|
||||||
// const ROM_FILE: &str = "./cpu_timing_test.nes";
|
// const ROM_FILE: &str = "./cpu_timing_test.nes";
|
||||||
// const ROM_FILE: &str = "../nes-test-roms/instr_test-v5/official_only.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;
|
extern crate nes_emu;
|
||||||
|
|
||||||
fn main() -> Result<(), iced::Error> {
|
fn main() -> Result<(), iced::Error> {
|
||||||
@@ -101,10 +123,12 @@ impl fmt::Display for HeaderButton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct Emulator {
|
struct Emulator {
|
||||||
|
running: bool,
|
||||||
nes: NES,
|
nes: NES,
|
||||||
windows: HashMap<Id, WindowType>,
|
windows: HashMap<Id, WindowType>,
|
||||||
debugger: DebuggerState,
|
debugger: DebuggerState,
|
||||||
main_win_size: Size,
|
main_win_size: Size,
|
||||||
|
prev: [Instant; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -117,6 +141,9 @@ enum Message {
|
|||||||
// DebugInt,
|
// DebugInt,
|
||||||
WindowClosed(Id),
|
WindowClosed(Id),
|
||||||
WindowOpened(Id),
|
WindowOpened(Id),
|
||||||
|
Key(keyboard::Event),
|
||||||
|
Periodic(Instant),
|
||||||
|
SetRunning(bool),
|
||||||
Header(HeaderButton),
|
Header(HeaderButton),
|
||||||
Hex(Id, HexEvent),
|
Hex(Id, HexEvent),
|
||||||
Debugger(DebuggerMessage),
|
Debugger(DebuggerMessage),
|
||||||
@@ -142,20 +169,30 @@ impl Emulator {
|
|||||||
min_size: None,
|
min_size: None,
|
||||||
..Settings::default()
|
..Settings::default()
|
||||||
});
|
});
|
||||||
|
let (win_2, task_2) = iced::window::open(Settings {
|
||||||
|
min_size: None,
|
||||||
|
..Settings::default()
|
||||||
|
});
|
||||||
(
|
(
|
||||||
Self {
|
Self {
|
||||||
nes,
|
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(),
|
debugger: DebuggerState::new(),
|
||||||
main_win_size: Size::new(0., 0.),
|
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 {
|
fn title(&self, win: Id) -> String {
|
||||||
match self.windows.get(&win) {
|
match self.windows.get(&win) {
|
||||||
Some(WindowType::Main) => "NES emu".into(),
|
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::TileMap) => "NES TileMap".into(),
|
||||||
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
|
Some(WindowType::TileViewer) => "NES Tile Viewer".into(),
|
||||||
Some(WindowType::Palette) => "NES Palette Viewer".into(),
|
Some(WindowType::Palette) => "NES Palette Viewer".into(),
|
||||||
@@ -266,6 +303,77 @@ impl Emulator {
|
|||||||
})
|
})
|
||||||
.discard();
|
.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());
|
// self.image.0.clone_from(self.nes.image());
|
||||||
Task::none()
|
Task::none()
|
||||||
@@ -275,10 +383,13 @@ impl Emulator {
|
|||||||
Subscription::batch([
|
Subscription::batch([
|
||||||
window::close_events().map(Message::WindowClosed),
|
window::close_events().map(Message::WindowClosed),
|
||||||
window::open_events().map(Message::WindowOpened),
|
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> {
|
fn view(&self, win: Id) -> Element<'_, Message> {
|
||||||
|
// println!("Running view");
|
||||||
match self.windows.get(&win) {
|
match self.windows.get(&win) {
|
||||||
Some(WindowType::Main) => {
|
Some(WindowType::Main) => {
|
||||||
let content = column![
|
let content = column![
|
||||||
@@ -300,7 +411,7 @@ impl Emulator {
|
|||||||
.render_cpu(self.nes.mem())
|
.render_cpu(self.nes.mem())
|
||||||
.map(move |e| Message::Hex(win, e)),
|
.map(move |e| Message::Hex(win, e)),
|
||||||
MemoryTy::PPU => view
|
MemoryTy::PPU => view
|
||||||
.render_ppu(self.nes.mem())
|
.render_ppu(self.nes.mem(), self.nes.ppu())
|
||||||
.map(move |e| Message::Hex(win, e)),
|
.map(move |e| Message::Hex(win, e)),
|
||||||
MemoryTy::OAM => view
|
MemoryTy::OAM => view
|
||||||
.render_oam(self.nes.ppu())
|
.render_oam(self.nes.ppu())
|
||||||
@@ -320,12 +431,22 @@ impl Emulator {
|
|||||||
Some(WindowType::Palette) => {
|
Some(WindowType::Palette) => {
|
||||||
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
|
dbg_image(DbgImage::Palette(self.nes.mem(), self.nes.ppu())).into()
|
||||||
}
|
}
|
||||||
Some(WindowType::Debugger) => {
|
Some(WindowType::Debugger) => column![
|
||||||
container(self.debugger.view(&self.nes).map(Message::Debugger))
|
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)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.into()
|
.into(),
|
||||||
}
|
|
||||||
None => panic!("Window not found"),
|
None => panic!("Window not found"),
|
||||||
// _ => todo!(),
|
// _ => todo!(),
|
||||||
}
|
}
|
||||||
@@ -364,6 +485,21 @@ impl Emulator {
|
|||||||
impl Program<Message> for Emulator {
|
impl Program<Message> for Emulator {
|
||||||
type State = ();
|
type State = ();
|
||||||
|
|
||||||
|
fn update(
|
||||||
|
&self,
|
||||||
|
_state: &mut Self::State,
|
||||||
|
_event: &iced::Event,
|
||||||
|
_bounds: Rectangle,
|
||||||
|
_cursor: iced::advanced::mouse::Cursor,
|
||||||
|
) -> Option<widget::Action<Message>> {
|
||||||
|
// ~ 60 fps, I think?
|
||||||
|
// Some(widget::Action::request_redraw_at(
|
||||||
|
// Instant::now() + Duration::from_millis(10),
|
||||||
|
// ))
|
||||||
|
// Some(widget::Action::request_redraw())
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
fn draw(
|
fn draw(
|
||||||
&self,
|
&self,
|
||||||
_state: &Self::State,
|
_state: &Self::State,
|
||||||
@@ -372,6 +508,7 @@ impl Program<Message> for Emulator {
|
|||||||
bounds: iced::Rectangle,
|
bounds: iced::Rectangle,
|
||||||
_cursor: mouse::Cursor,
|
_cursor: mouse::Cursor,
|
||||||
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
) -> Vec<iced::widget::canvas::Geometry<Renderer>> {
|
||||||
|
// let start = Instant::now();
|
||||||
// const SIZE: f32 = 2.;
|
// const SIZE: f32 = 2.;
|
||||||
let mut frame = Frame::new(
|
let mut frame = Frame::new(
|
||||||
renderer,
|
renderer,
|
||||||
@@ -381,16 +518,28 @@ impl Program<Message> for Emulator {
|
|||||||
// },
|
// },
|
||||||
);
|
);
|
||||||
frame.scale(2.);
|
frame.scale(2.);
|
||||||
for y in 0..240 {
|
// TODO: use image for better? performance
|
||||||
for x in 0..256 {
|
frame.draw_image(
|
||||||
let c = self.nes.image().read(y, x);
|
Rectangle::new(Point::new(0., 0.), Size::new(256., 240.)),
|
||||||
frame.fill_rectangle(
|
widget::canvas::Image::new(widget::image::Handle::from_rgba(
|
||||||
Point::new(x as f32, y as f32),
|
256,
|
||||||
Size::new(1., 1.),
|
240,
|
||||||
Color::from_rgb8(c.r, c.g, c.b),
|
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()]
|
vec![frame.into_geometry()]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
src/mem.rs
32
src/mem.rs
@@ -6,13 +6,13 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Value<'a, R> {
|
pub enum Value<R> {
|
||||||
Value(u8),
|
Value(u8),
|
||||||
Register { reg: &'a R, offset: u16 },
|
Register { reg: R, offset: u16 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R> Value<'_, R> {
|
impl<R> Value<R> {
|
||||||
pub fn reg_map(self, f: impl FnOnce(&R, u16) -> u8) -> u8 {
|
pub fn reg_map(self, f: impl FnOnce(R, u16) -> u8) -> u8 {
|
||||||
match self {
|
match self {
|
||||||
Value::Value(v) => v,
|
Value::Value(v) => v,
|
||||||
Value::Register { reg, offset } => f(reg, offset),
|
Value::Register { reg, offset } => f(reg, offset),
|
||||||
@@ -117,7 +117,7 @@ pub struct MemoryMap<R> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Copy> MemoryMap<R> {
|
impl<R: Copy> MemoryMap<R> {
|
||||||
pub fn read(&self, addr: u16) -> Value<'_, R> {
|
pub fn read(&self, addr: u16) -> Value<R> {
|
||||||
// self.edit_ver += 1;
|
// self.edit_ver += 1;
|
||||||
for segment in &self.segments {
|
for segment in &self.segments {
|
||||||
if segment.position <= addr && addr - segment.position < segment.size {
|
if segment.position <= addr && addr - segment.position < segment.size {
|
||||||
@@ -125,7 +125,7 @@ impl<R: Copy> MemoryMap<R> {
|
|||||||
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
Data::RAM(items) => Value::Value(items[(addr - segment.position) as usize]),
|
||||||
Data::ROM(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 {
|
Data::Reg(reg) => Value::Register {
|
||||||
reg,
|
reg: *reg,
|
||||||
offset: addr - segment.position,
|
offset: addr - segment.position,
|
||||||
},
|
},
|
||||||
Data::Mirror(pos) => {
|
Data::Mirror(pos) => {
|
||||||
@@ -201,28 +201,28 @@ impl<R: Copy> MemoryMap<R> {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn peek_val(&self, addr: u16) -> Option<u8> {
|
fn peek_val(&self, addr: u16) -> Result<u8, Option<(R, u16)>> {
|
||||||
for segment in &self.segments {
|
for segment in &self.segments {
|
||||||
if segment.position <= addr && addr - segment.position < segment.size {
|
if segment.position <= addr && addr - segment.position < segment.size {
|
||||||
return match &segment.mem {
|
return match &segment.mem {
|
||||||
Data::RAM(items) => Some(items[(addr - segment.position) as usize]),
|
Data::RAM(items) => Ok(items[(addr - segment.position) as usize]),
|
||||||
Data::ROM(items) => Some(items[(addr - segment.position) as usize]),
|
Data::ROM(items) => Ok(items[(addr - segment.position) as usize]),
|
||||||
Data::Reg(_) => None,
|
Data::Reg(r) => Err(Some((*r, addr - segment.position))),
|
||||||
Data::Mirror(pos) => {
|
Data::Mirror(pos) => {
|
||||||
let offset = addr - segment.position;
|
let offset = addr - segment.position;
|
||||||
self.peek_val(pos + offset)
|
self.peek_val(pos + offset)
|
||||||
}
|
}
|
||||||
Data::Disabled => None,
|
Data::Disabled => Err(None),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
Err(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R: Copy> Memory for MemoryMap<R> {
|
impl<R: Copy> Memory for MemoryMap<R> {
|
||||||
fn peek(&self, addr: usize) -> Option<u8> {
|
fn peek(&self, addr: usize) -> Option<u8> {
|
||||||
self.peek_val(addr as u16)
|
self.peek_val(addr as u16).ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
fn len(&self) -> usize {
|
||||||
@@ -455,14 +455,14 @@ impl Mapped {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn peek_ppu(&self, addr: u16) -> Option<u8> {
|
pub fn peek_ppu(&self, addr: u16) -> Result<u8, Option<(PPUMMRegisters, u16)>> {
|
||||||
self.ppu.peek_val(addr)
|
self.ppu.peek_val(addr)
|
||||||
}
|
}
|
||||||
pub fn ppu_edit_ver(&self) -> usize {
|
pub fn ppu_edit_ver(&self) -> usize {
|
||||||
self.ppu.edit_ver
|
self.ppu.edit_ver
|
||||||
}
|
}
|
||||||
pub fn peek_cpu(&self, addr: u16) -> Option<u8> {
|
pub fn peek_cpu(&self, addr: u16) -> Option<u8> {
|
||||||
self.cpu.peek_val(addr)
|
self.cpu.peek_val(addr).ok()
|
||||||
}
|
}
|
||||||
pub fn cpu_edit_ver(&self) -> usize {
|
pub fn cpu_edit_ver(&self) -> usize {
|
||||||
self.cpu.edit_ver
|
self.cpu.edit_ver
|
||||||
@@ -638,7 +638,7 @@ impl<'a> PpuMem<'a> {
|
|||||||
pub fn new(mem: &'a mut Mapped) -> Self {
|
pub fn new(mem: &'a mut Mapped) -> Self {
|
||||||
Self(mem)
|
Self(mem)
|
||||||
}
|
}
|
||||||
pub fn read(&mut self, addr: u16) -> Value<'_, PPUMMRegisters> {
|
pub fn read(&mut self, addr: u16) -> Value<PPUMMRegisters> {
|
||||||
self.0.ppu.read(addr)
|
self.0.ppu.read(addr)
|
||||||
}
|
}
|
||||||
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&PPUMMRegisters, u16, u8)) {
|
pub fn write(&mut self, addr: u16, val: u8, reg_fn: impl FnOnce(&PPUMMRegisters, u16, u8)) {
|
||||||
|
|||||||
77
src/ppu.rs
77
src/ppu.rs
@@ -1,15 +1,13 @@
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
use bytes::{Bytes, BytesMut};
|
||||||
use iced::{
|
use iced::{
|
||||||
Point, Size,
|
Point, Size,
|
||||||
advanced::graphics::geometry::Renderer,
|
advanced::graphics::geometry::Renderer,
|
||||||
widget::canvas::{Fill, Frame},
|
widget::canvas::{Fill, Frame},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::mem::{Mapped, PpuMem, Value};
|
||||||
hex_view::Memory,
|
|
||||||
mem::{Mapped, PpuMem},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub struct Color {
|
pub struct Color {
|
||||||
@@ -33,18 +31,25 @@ impl fmt::Display for Color {
|
|||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||||
pub struct RenderBuffer<const W: usize, const H: usize> {
|
pub struct RenderBuffer<const W: usize, const H: usize> {
|
||||||
buffer: Box<[Color]>,
|
buffer: Box<[Color]>,
|
||||||
|
raw_rgba: BytesMut,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
||||||
pub fn empty() -> Self {
|
pub fn empty() -> Self {
|
||||||
Self {
|
Self {
|
||||||
buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(),
|
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) {
|
pub fn write(&mut self, line: usize, pixel: usize, color: Color) {
|
||||||
assert!(line < H && pixel < W);
|
assert!(line < H && pixel < W);
|
||||||
self.buffer[line * W + pixel] = color;
|
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 {
|
pub fn read(&self, line: usize, pixel: usize) -> Color {
|
||||||
assert!(line < H && pixel < W);
|
assert!(line < H && pixel < W);
|
||||||
@@ -53,6 +58,12 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
|||||||
|
|
||||||
pub fn clone_from(&mut self, other: &Self) {
|
pub fn clone_from(&mut self, other: &Self) {
|
||||||
self.buffer.copy_from_slice(&other.buffer);
|
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) {
|
pub fn assert_eq(&self, other: &Self) {
|
||||||
@@ -72,7 +83,7 @@ impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum PPUMMRegisters {
|
pub enum PPUMMRegisters {
|
||||||
Palette,
|
Palette,
|
||||||
}
|
}
|
||||||
@@ -84,18 +95,6 @@ pub struct OAM {
|
|||||||
edit_ver: usize,
|
edit_ver: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
enum BackgroundState {
|
|
||||||
NameTableBytePre,
|
|
||||||
NameTableByte,
|
|
||||||
AttrTableBytePre,
|
|
||||||
AttrTableByte,
|
|
||||||
PatternTableTileLowPre,
|
|
||||||
PatternTableTileLow,
|
|
||||||
PatternTableTileHighPre,
|
|
||||||
PatternTableTileHigh,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Background {
|
pub struct Background {
|
||||||
/// Current vram address, 15 bits
|
/// Current vram address, 15 bits
|
||||||
@@ -112,8 +111,6 @@ pub struct Background {
|
|||||||
pub vram_column: bool,
|
pub vram_column: bool,
|
||||||
pub second_pattern: bool,
|
pub second_pattern: bool,
|
||||||
|
|
||||||
state: BackgroundState,
|
|
||||||
|
|
||||||
pub cur_nametable: u8,
|
pub cur_nametable: u8,
|
||||||
pub cur_attr: u8,
|
pub cur_attr: u8,
|
||||||
pub next_attr: u8,
|
pub next_attr: u8,
|
||||||
@@ -470,6 +467,10 @@ impl Palette {
|
|||||||
debug_assert!(idx < 0x20, "Palette index out of range");
|
debug_assert!(idx < 0x20, "Palette index out of range");
|
||||||
self.colors[(self.ram[idx as usize + palette as usize * 4] & 0x3F) as usize]
|
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)]
|
#[derive(Clone)]
|
||||||
@@ -482,10 +483,12 @@ pub struct PPU {
|
|||||||
pub scanline: usize,
|
pub scanline: usize,
|
||||||
pub pixel: usize,
|
pub pixel: usize,
|
||||||
|
|
||||||
|
vram_buffer: u8,
|
||||||
|
|
||||||
pub mask: Mask,
|
pub mask: Mask,
|
||||||
pub vblank: bool,
|
pub vblank: bool,
|
||||||
|
|
||||||
palette: Palette,
|
pub palette: Palette,
|
||||||
pub background: Background,
|
pub background: Background,
|
||||||
oam: OAM,
|
oam: OAM,
|
||||||
pub render_buffer: RenderBuffer<256, 240>,
|
pub render_buffer: RenderBuffer<256, 240>,
|
||||||
@@ -542,6 +545,7 @@ impl PPU {
|
|||||||
frame_count: 0,
|
frame_count: 0,
|
||||||
nmi_enabled: false,
|
nmi_enabled: false,
|
||||||
// nmi_waiting: false,
|
// nmi_waiting: false,
|
||||||
|
// TODO: Is even in the right initial state?
|
||||||
even: false,
|
even: false,
|
||||||
scanline: 0,
|
scanline: 0,
|
||||||
pixel: 25,
|
pixel: 25,
|
||||||
@@ -553,7 +557,6 @@ impl PPU {
|
|||||||
w: false,
|
w: false,
|
||||||
vram_column: false,
|
vram_column: false,
|
||||||
second_pattern: false,
|
second_pattern: false,
|
||||||
state: BackgroundState::NameTableBytePre,
|
|
||||||
cur_high: 0,
|
cur_high: 0,
|
||||||
cur_low: 0,
|
cur_low: 0,
|
||||||
cur_shift_high: 0,
|
cur_shift_high: 0,
|
||||||
@@ -568,6 +571,7 @@ impl PPU {
|
|||||||
addr: 0,
|
addr: 0,
|
||||||
edit_ver: 0,
|
edit_ver: 0,
|
||||||
},
|
},
|
||||||
|
vram_buffer: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
@@ -593,10 +597,22 @@ impl PPU {
|
|||||||
5 => panic!("ppuscroll is write-only"),
|
5 => panic!("ppuscroll is write-only"),
|
||||||
6 => panic!("ppuaddr is write-only"),
|
6 => panic!("ppuaddr is write-only"),
|
||||||
7 => {
|
7 => {
|
||||||
|
// TODO: read buffer only applies to ppu data, not palette ram...
|
||||||
// println!("Updating v for ppudata read");
|
// println!("Updating v for ppudata read");
|
||||||
let val = mem.read(self.background.v).reg_map(|a, off| match a {
|
// self.vram_buffer =
|
||||||
PPUMMRegisters::Palette => self.palette.ram[off as usize],
|
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
|
// if self.background
|
||||||
self.increment_v();
|
self.increment_v();
|
||||||
val
|
val
|
||||||
@@ -656,7 +672,7 @@ impl PPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
0x06 => {
|
0x06 => {
|
||||||
// TODO: ppu addr
|
// TODO: this actually sets T, which is copied to v later (~ a pixel later?)
|
||||||
if self.background.w {
|
if self.background.w {
|
||||||
self.background.v =
|
self.background.v =
|
||||||
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
|
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
|
||||||
@@ -672,10 +688,16 @@ impl PPU {
|
|||||||
}
|
}
|
||||||
0x07 => {
|
0x07 => {
|
||||||
// println!("Writing: {:02X}, @{:04X}", val, self.background.v);
|
// 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 => {
|
PPUMMRegisters::Palette => {
|
||||||
// println!("Writing {:02X} to {:02X}", v & 0x3F, o);
|
// println!("Writing {:02X} to {:02X}", v & 0x3F, o);
|
||||||
|
if o % 4 == 0 {
|
||||||
|
o = o & !0b1_0000;
|
||||||
self.palette.ram[o as usize] = v & 0x3F;
|
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();
|
self.increment_v();
|
||||||
@@ -710,6 +732,7 @@ impl PPU {
|
|||||||
self.scanline = 0;
|
self.scanline = 0;
|
||||||
self.pixel = 0;
|
self.pixel = 0;
|
||||||
self.even = !self.even;
|
self.even = !self.even;
|
||||||
|
self.frame_count += 1;
|
||||||
}
|
}
|
||||||
if self.pixel == 341 {
|
if self.pixel == 341 {
|
||||||
self.pixel = 0;
|
self.pixel = 0;
|
||||||
@@ -849,8 +872,6 @@ impl PPU {
|
|||||||
}
|
}
|
||||||
if self.scanline == 241 && self.pixel == 1 {
|
if self.scanline == 241 && self.pixel == 1 {
|
||||||
self.vblank = true;
|
self.vblank = true;
|
||||||
self.frame_count += 1;
|
|
||||||
self.background.state = BackgroundState::NameTableBytePre;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ reset:
|
|||||||
stx $2000 ; Disable NMI (by writing zero)
|
stx $2000 ; Disable NMI (by writing zero)
|
||||||
stx $4010 ; Disable DMC IRQs
|
stx $4010 ; Disable DMC IRQs
|
||||||
ldx #$08
|
ldx #$08
|
||||||
stx $2001 ; Disable rendering
|
stx $2001 ; Enable rendering
|
||||||
|
|
||||||
bit $2002 ; Clear vblank flag by reading ppu status
|
bit $2002 ; Clear vblank flag by reading ppu status
|
||||||
VBLANKWAIT1:
|
VBLANKWAIT1:
|
||||||
|
|||||||
@@ -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| {
|
rom_test!(ppu_vertical_write, "ppu_vertical_write.nes", |nes| {
|
||||||
assert_eq!(nes.last_instruction(), "0x8040 HLT :2 []");
|
assert_eq!(nes.last_instruction(), "0x8040 HLT :2 []");
|
||||||
|
|
||||||
assert_eq!(nes.mem().peek_ppu(0x2000), Some(1));
|
assert_eq!(nes.mem().peek_ppu(0x2000), Ok(1));
|
||||||
assert_eq!(nes.mem().peek_ppu(0x2020), Some(1));
|
assert_eq!(nes.mem().peek_ppu(0x2020), Ok(1));
|
||||||
assert_eq!(nes.mem().peek_ppu(0x2400), Some(2));
|
assert_eq!(nes.mem().peek_ppu(0x2400), Ok(2));
|
||||||
assert_eq!(nes.mem().peek_ppu(0x2401), Some(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 []");
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
70
src/test_roms/ppu_palette_shared.s
Normal file
70
src/test_roms/ppu_palette_shared.s
Normal file
@@ -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
|
||||||
96
src/test_roms/render-updating.s
Normal file
96
src/test_roms/render-updating.s
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user