rustfmt it all

This commit is contained in:
Henrik Persson 2023-04-12 11:52:27 +02:00 committed by Henrik Persson
parent 7c49ab8807
commit b9ccc3fe8e
55 changed files with 1524 additions and 932 deletions

7
.rustfmt.toml Normal file
View file

@ -0,0 +1,7 @@
# These settings are not supported on stable Rust, and are ignored by the ninja
# build script - to use them you need to run 'cargo +nightly fmt'
group_imports = "StdExternalCrate"
imports_granularity = "Item"
imports_layout = "Vertical"
#wrap_comments = true
tab_spaces = 2

View file

@ -20,13 +20,12 @@ pub mod bits {
pub fn is_signed(n: u8) -> bool {
n & (1 << 7) != 0
}
pub fn is_overflow(res: u8, lhs: u8, rhs: u8) -> bool {
if is_signed(lhs) && is_signed(rhs) && !is_signed(res) {
true
}
else {
!is_signed(lhs) && !is_signed(rhs) && is_signed(res)
} else {
!is_signed(lhs) && !is_signed(rhs) && is_signed(res)
}
}
}
}

View file

@ -1,6 +1,8 @@
use alloc::boxed::Box;
use crate::{cpu::{Cpu, Reg}, memory::Bus};
use crate::cpu::Cpu;
use crate::cpu::Reg;
use crate::memory::Bus;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AddressMode {
@ -20,13 +22,17 @@ pub enum AddressMode {
}
impl AddressMode {
pub fn resolve(&self, cpu: &mut Cpu, operands: (Option<u8>, Option<u8>), num_extra_cycles: usize) -> u16 {
pub fn resolve(
&self,
cpu: &mut Cpu,
operands: (Option<u8>, Option<u8>),
num_extra_cycles: usize,
) -> u16 {
let mem = cpu.bus();
if self.is_zeropage() {
self.resolve_zeropage(cpu, operands.0.unwrap(), num_extra_cycles)
}
else {
} else {
let low = operands.0.unwrap();
let high = operands.1.unwrap();
let address: u16 = ((high as u16) << 8) | low as u16;
@ -36,7 +42,7 @@ impl AddressMode {
AddressMode::AbsX => self.cycle_aware_add(cpu, address, cpu[Reg::X], num_extra_cycles),
AddressMode::AbsY => self.cycle_aware_add(cpu, address, cpu[Reg::Y], num_extra_cycles),
AddressMode::Ind => self.read16(mem, low, high),
_ => panic!()
_ => panic!(),
}
}
}
@ -53,7 +59,7 @@ impl AddressMode {
AddressMode::Zero => operand as u16,
AddressMode::ZeroX => operand.wrapping_add(cpu[Reg::X]) as u16, // Zeropage
AddressMode::ZeroY => operand.wrapping_add(cpu[Reg::Y]) as u16, // zeropage
_ => panic!()
_ => panic!(),
}
}
@ -76,12 +82,13 @@ impl AddressMode {
}
fn is_zeropage(&self) -> bool {
matches!(self,
AddressMode::IndX |
AddressMode::IndY |
AddressMode::Zero |
AddressMode::ZeroX |
AddressMode::ZeroY
matches!(
self,
AddressMode::IndX
| AddressMode::IndY
| AddressMode::Zero
| AddressMode::ZeroX
| AddressMode::ZeroY
)
}
}
}

View file

@ -1,15 +1,18 @@
use core::ops::{Index, IndexMut};
use alloc::boxed::Box;
use core::ops::Index;
use core::ops::IndexMut;
use crate::address_mode::AddressMode;
use crate::instructions::Instruction;
use crate::instructions::Opcode;
use crate::memory::Bus;
use crate::instructions::{Instruction, Opcode};
#[derive(Debug, Clone, Copy)]
pub enum Reg {
AC = 0,
X = 1,
Y = 2,
SP = 3
SP = 3,
}
#[derive(Debug, Clone, Copy)]
@ -46,8 +49,8 @@ impl Cpu {
const RESET_VECTOR: u16 = 0xfffc;
const IRQ_VECTOR: u16 = 0xfffe;
pub fn new<T : Bus + 'static>(mem: T) -> Self {
Self {
pub fn new<T: Bus + 'static>(mem: T) -> Self {
Self {
pc: 0,
flags: [0; 8],
regs: [0; 4],
@ -84,7 +87,7 @@ impl Cpu {
if inst.mode() == AddressMode::AbsX {
let _ = inst.resolve_operand_value(self);
}
},
}
Opcode::DEX => self.dec_reg(Reg::X),
Opcode::DEY => self.dec_reg(Reg::Y),
Opcode::INX => self.inc_reg(Reg::X),
@ -164,7 +167,7 @@ impl Cpu {
Opcode::JMP => {
let target = inst.resolve_operand_address(self);
if inst.mode() == AddressMode::Ind {
self.set_pc(target + 1);
self.set_pc(target + 1);
}
self.set_pc(target);
}
@ -231,11 +234,11 @@ impl Cpu {
self.cmp(Reg::AC, val);
}
Opcode::SRE => {
// LSR oper
// LSR oper
let (val, address) = inst.resolve_operand_value_and_address(self);
let res = self.shift_right(val);
self.mem.write8(res, address);
// EOR oper
let res = self[Reg::AC] ^ res;
self[Reg::AC] = res;
@ -356,7 +359,7 @@ impl Cpu {
self.flags_set_neg_zero(res);
}
Opcode::AND => {
let val = inst.resolve_operand_value( self);
let val = inst.resolve_operand_value(self);
let res = self[Reg::AC] & val;
self[Reg::AC] = res;
self.flags_set_neg_zero(res);
@ -413,7 +416,7 @@ impl Cpu {
// No jmp; advance.
// TODO: I was really wrong here, JMP to PC is legit (a way to wait for nmi or something?)
// Added tmp check for JMP, should find a better way.
// Added tmp check for JMP, should find a better way.
// Probably more opcodes than JMP affected.. branch ops??
if pc_before_exec == self.pc() && inst.opcode() != Opcode::JMP {
self.inc_pc(inst.size());
@ -567,8 +570,7 @@ impl Cpu {
if signed >= 0 {
let effective_offset = offset as u16;
self.pc.wrapping_add(effective_offset)
}
else {
} else {
let signed_offset = ((offset as u16) | 0xff00) as i16;
let effective_offset = (-signed_offset) as u16;
self.pc.wrapping_sub(effective_offset)
@ -593,7 +595,7 @@ impl Cpu {
self.add_with_carry(lhs, rhs ^ 0xff)
}
fn push(&mut self, val: u8) {
fn push(&mut self, val: u8) {
let sp = self[Reg::SP] as usize;
let address = (Cpu::STACK_TOP + sp) as u16;
self.mem.write8(val, address);
@ -611,7 +613,7 @@ impl Cpu {
for bit in 0..=7usize {
match bit {
5 | 4 => (), // ignore break and 5
_ => self.flags[bit] = if val & (1 << bit) == 0 { 0 } else { 1 }
_ => self.flags[bit] = if val & (1 << bit) == 0 { 0 } else { 1 },
}
}
}
@ -632,7 +634,7 @@ impl Cpu {
if cond {
self.inc_pc(2);
let branch_target = self.calc_offset_pc(offset);
// if hi byte changes, we crossed a page boundary and should add extra cycles
// "add 1 to cycles if branch occurs on same page, add 2 to cycles if branch occurs to different page"
let crossed_page = self.pc() & 0xff00 != branch_target & 0xff00;
@ -792,7 +794,7 @@ mod tests {
#[test]
fn cmp() {
let mut cpu = sut();
cpu[Reg::Y] = 10;
cpu.cmp(Reg::Y, 11);
assert_eq!(cpu[Flag::Z], 0);
@ -844,4 +846,4 @@ mod tests {
cpu.set_pc(0x0000);
assert_eq!(cpu.calc_offset_pc(0xc1), 0xffc1); // -63
}
}
}

View file

@ -1,7 +1,15 @@
use alloc::vec::Vec;
use std::collections::VecDeque;
use std::fmt::Write;
use std::ops::RangeInclusive;
use getch::Getch;
use std::{fmt::Write, collections::{VecDeque}, ops::RangeInclusive};
use crate::{cpu::{Cpu, Reg, Flag}, instructions::{Instruction, Opcode}};
use crate::cpu::Cpu;
use crate::cpu::Flag;
use crate::cpu::Reg;
use crate::instructions::Instruction;
use crate::instructions::Opcode;
const BACKTRACE_LIMIT: usize = 11;
@ -18,29 +26,36 @@ pub struct Debugger {
struct BacktraceEntry {
inst: Instruction,
pc: u16,
opbyte: u8
opbyte: u8,
}
#[derive(PartialEq, Eq)]
pub enum Breakpoint {
Address(u16),
Opcode(String),
OpcodeSequence(Vec<&'static str>)
// TODO add support to break on opcode WITH operands
OpcodeSequence(Vec<&'static str>), // TODO add support to break on opcode WITH operands
}
enum Watch {
Range { address: RangeInclusive<u16>, state: Option<Vec<u8>>, f: Box<dyn Fn(Vec<u8>)> },
Address { address: u16, state: Option<u8>, f: Box<dyn Fn(u8)> },
Range {
address: RangeInclusive<u16>,
state: Option<Vec<u8>>,
f: Box<dyn Fn(Vec<u8>)>,
},
Address {
address: u16,
state: Option<u8>,
f: Box<dyn Fn(u8)>,
},
// TODO: Reg, Flag, PC watches
}
impl Debugger {
pub fn default() -> Self {
Self {
stdin: Getch::new(),
Self {
stdin: Getch::new(),
breakpoints: Vec::with_capacity(2),
last_pc: None,
last_pc: None,
suspended: false,
verbose: false,
backtrace: VecDeque::with_capacity(BACKTRACE_LIMIT),
@ -64,11 +79,15 @@ impl Debugger {
let pc = cpu.pc();
let opbyte = cpu.bus().read8(pc);
self.backtrace.push_back(BacktraceEntry { inst: next_inst.clone(), pc, opbyte });
self.backtrace.push_back(BacktraceEntry {
inst: next_inst.clone(),
pc,
opbyte,
});
if self.backtrace.len() == BACKTRACE_LIMIT {
self.backtrace.remove(0);
}
if self.suspended || self.verbose {
Debugger::print_instruction(pc, opbyte, &next_inst);
}
@ -77,8 +96,7 @@ impl Debugger {
if self.suspended {
self.user_input(cpu);
}
else if self.is_breakpoint(pc, next_inst.opcode()) {
} else if self.is_breakpoint(pc, next_inst.opcode()) {
self.suspend(cpu, pc);
}
@ -90,40 +108,47 @@ impl Debugger {
match b {
Breakpoint::Address(addr) => {
if *addr == pc {
return true
return true;
}
},
}
Breakpoint::Opcode(opstr) => {
if *opstr == opcode.to_string() {
return true;
}
},
}
Breakpoint::OpcodeSequence(seq) => {
let history: Vec<String> = self.backtrace.iter()
let history: Vec<String> = self
.backtrace
.iter()
.rev()
.take(seq.len())
.map(|b| b.inst.opcode().to_string())
.collect();
let upper: Vec<String> = seq.iter()
.rev()
.map(|&s| s.to_uppercase())
.collect();
let upper: Vec<String> = seq.iter().rev().map(|&s| s.to_uppercase()).collect();
if history == upper {
return true;
}
},
}
}
}
false
}
pub fn watch_memory_range(&mut self, range: RangeInclusive<u16>, f: impl Fn(Vec<u8>) + 'static) {
let watch = Watch::Range { address: range, state: None, f: Box::new(f) };
let watch = Watch::Range {
address: range,
state: None,
f: Box::new(f),
};
self.watches.push(watch)
}
pub fn watch_memory(&mut self, address: u16, f: impl Fn(u8) + 'static) {
let watch = Watch::Address { address, state: None, f: Box::new(f) };
let watch = Watch::Address {
address,
state: None,
f: Box::new(f),
};
self.watches.push(watch)
}
@ -156,7 +181,8 @@ impl Debugger {
}
}
pub fn enable(&mut self) { // TODO: better API
pub fn enable(&mut self) {
// TODO: better API
self.dump_backtrace();
self.suspended = true;
}
@ -179,7 +205,8 @@ impl Debugger {
let ch = self.stdin.getch().unwrap();
match ch {
0x20 => (), // Space, step
0x0a => { // Enter
0x0a => {
// Enter
println!("{:?}", cpu);
println!("{}", cpu);
self.user_input(cpu);
@ -218,20 +245,30 @@ impl Debugger {
fn print_instruction(pc: u16, opbyte: u8, inst: &Instruction) {
let mut pc_str = String::new();
write!(&mut pc_str, "{:#06x}", pc).unwrap();
let mut opbyte_str = String::new();
write!(&mut opbyte_str, "{:#04x}", opbyte).unwrap();
let mut operands_str = String::new();
let operands = [inst.operands().0, inst.operands().1];
for operand in operands.iter().filter_map(|o| *o) {
write!(&mut operands_str, "{:#04x} ", operand).unwrap();
}
let mut mnemonic_str = String::new();
write!(&mut mnemonic_str, "{:?} {:?} {}", inst.opcode(), inst.mode(), operands_str).unwrap();
println!("{:<10} {} {:<10} {}", pc_str, opbyte_str, operands_str, mnemonic_str);
write!(
&mut mnemonic_str,
"{:?} {:?} {}",
inst.opcode(),
inst.mode(),
operands_str
)
.unwrap();
println!(
"{:<10} {} {:<10} {}",
pc_str, opbyte_str, operands_str, mnemonic_str
);
}
}
@ -242,15 +279,42 @@ impl std::fmt::Debug for Cpu {
}
write!(f, "--------\n")?;
write!(f, "pc:\t{:#06x}\nsp:\t{}\nacc:\t{}\nx:\t{}\ny:\t{}\n", self.pc(), hexdec(self[Reg::SP]), hexdec(self[Reg::AC]), hexdec(self[Reg::X]), hexdec(self[Reg::Y]))?;
write!(f, "NEG={}, OVF={}, DEC={}, INT={}, ZER={}, CAR={} ({:#010b}) ({:#04x})", self[Flag::N], self[Flag::V], self[Flag::D], self[Flag::I], self[Flag::Z], self[Flag::C], self.flags_as_byte(), self.flags_as_byte())
write!(
f,
"pc:\t{:#06x}\nsp:\t{}\nacc:\t{}\nx:\t{}\ny:\t{}\n",
self.pc(),
hexdec(self[Reg::SP]),
hexdec(self[Reg::AC]),
hexdec(self[Reg::X]),
hexdec(self[Reg::Y])
)?;
write!(
f,
"NEG={}, OVF={}, DEC={}, INT={}, ZER={}, CAR={} ({:#010b}) ({:#04x})",
self[Flag::N],
self[Flag::V],
self[Flag::D],
self[Flag::I],
self[Flag::Z],
self[Flag::C],
self.flags_as_byte(),
self.flags_as_byte()
)
}
}
impl std::fmt::Display for Cpu {
// nestest format
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}", self[Reg::AC], self[Reg::X], self[Reg::Y], self.flags_as_byte(), self[Reg::SP])
write!(
f,
"A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X}",
self[Reg::AC],
self[Reg::X],
self[Reg::Y],
self.flags_as_byte(),
self[Reg::SP]
)
}
}
@ -258,4 +322,4 @@ impl std::fmt::Display for Opcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
}

View file

@ -1,4 +1,5 @@
use crate::{address_mode::AddressMode, cpu::Cpu};
use crate::address_mode::AddressMode;
use crate::cpu::Cpu;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[allow(clippy::upper_case_acronyms)]
@ -62,22 +63,22 @@ pub enum Opcode {
RTI, // Return from Interrupt
// Illegal opcodes
LAX, // LDA oper + LDX oper
SAX, // A AND X -> M
LAX, // LDA oper + LDX oper
SAX, // A AND X -> M
USBC, // effectively same as normal SBC immediate, instr. E9.
DCP, // DEC oper + CMP oper
ISC, // INC oper + SBC oper
SLO, // ASL oper + ORA oper
RLA, // ROL oper + AND oper
SRE, // LSR oper + EOR oper
RRA, // ROR oper + ADC oper
PHX, // push x
PHY, // push y
PLX, // pull x
PLY, // pull y
ANC, // AND oper + set C as ASL
DCP, // DEC oper + CMP oper
ISC, // INC oper + SBC oper
SLO, // ASL oper + ORA oper
RLA, // ROL oper + AND oper
SRE, // LSR oper + EOR oper
RRA, // ROR oper + ADC oper
PHX, // push x
PHY, // push y
PLX, // pull x
PLY, // pull y
ANC, // AND oper + set C as ASL
ANC2, // effectively the same as instr. 0B (ANC)
ALR, // AND oper + LSR
ALR, // AND oper + LSR
}
#[derive(Clone, Debug)]
@ -91,15 +92,33 @@ pub struct Instruction {
impl Instruction {
fn imp(opcode: Opcode, cycles: usize) -> Self {
Self { opcode, cycles, mode: AddressMode::Impl, size: 1, operands: (None, None) }
Self {
opcode,
cycles,
mode: AddressMode::Impl,
size: 1,
operands: (None, None),
}
}
fn two(opcode: Opcode, cycles: usize, mode: AddressMode, operand: u8) -> Self {
Self { opcode, cycles, mode, size: 2, operands: (Some(operand), None) }
Self {
opcode,
cycles,
mode,
size: 2,
operands: (Some(operand), None),
}
}
fn thr(opcode: Opcode, cycles: usize, mode: AddressMode, operand0: u8, operand1: u8) -> Self {
Self { opcode, cycles, mode, size: 3, operands: (Some(operand0), Some(operand1)) }
Self {
opcode,
cycles,
mode,
size: 3,
operands: (Some(operand0), Some(operand1)),
}
}
pub fn size(&self) -> u8 {
@ -119,7 +138,9 @@ impl Instruction {
}
pub fn resolve_operand_value_and_address(&self, cpu: &mut Cpu) -> (u8, u16) {
let address = self.mode.resolve(cpu, self.operands, self.num_extra_cycles());
let address = self
.mode
.resolve(cpu, self.operands, self.num_extra_cycles());
let value = cpu.bus().read8(address);
(value, address)
}
@ -128,14 +149,18 @@ impl Instruction {
match self.mode {
AddressMode::Imm => self.operands.0.unwrap(),
_ => {
let address = self.mode.resolve(cpu, self.operands, self.num_extra_cycles());
let address = self
.mode
.resolve(cpu, self.operands, self.num_extra_cycles());
cpu.bus().read8(address)
}
}
}
pub fn resolve_operand_address(&self, cpu: &mut Cpu) -> u16 {
self.mode.resolve(cpu, self.operands, self.num_extra_cycles())
self
.mode
.resolve(cpu, self.operands, self.num_extra_cycles())
}
pub fn operand0(&self) -> u8 {
@ -156,19 +181,19 @@ impl Instruction {
Opcode::SRE => 0,
Opcode::RRA => 0,
// isc in indy apparently adds 4 cycles.. bc many instructions in one i guess
Opcode::ISC => {
match self.mode {
AddressMode::IndY => 4,
_ => 0,
}
}
Opcode::ISC => match self.mode {
AddressMode::IndY => 4,
_ => 0,
},
_ => 1,
}
}
pub fn disassemble(opbyte: u8, operand1: u8, operand2: u8) -> Instruction {
match opbyte {
0x02 | 0x12 | 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => Instruction::imp(Opcode::JAM, 0),
0x02 | 0x12 | 0x22 | 0x32 | 0x42 | 0x52 | 0x62 | 0x72 | 0x92 | 0xB2 | 0xD2 | 0xF2 => {
Instruction::imp(Opcode::JAM, 0)
}
0x00 => Instruction::imp(Opcode::BRK, 7),
0x40 => Instruction::imp(Opcode::RTI, 6),
@ -176,12 +201,12 @@ impl Instruction {
0x38 => Instruction::imp(Opcode::SEC, 2),
0xf8 => Instruction::imp(Opcode::SED, 2),
0x78 => Instruction::imp(Opcode::SEI, 2),
0x18 => Instruction::imp(Opcode::CLC, 2),
0xd8 => Instruction::imp(Opcode::CLD, 2),
0x58 => Instruction::imp(Opcode::CLI, 2),
0xb8 => Instruction::imp(Opcode::CLV, 2),
0xaa => Instruction::imp(Opcode::TAX, 2),
0xa8 => Instruction::imp(Opcode::TAY, 2),
0xba => Instruction::imp(Opcode::TSX, 2),
@ -191,7 +216,7 @@ impl Instruction {
0x24 => Instruction::two(Opcode::BIT, 3, AddressMode::Zero, operand1),
0x2c => Instruction::thr(Opcode::BIT, 4, AddressMode::Abs, operand1, operand2),
0x69 => Instruction::two(Opcode::ADC, 2, AddressMode::Imm, operand1),
0x65 => Instruction::two(Opcode::ADC, 3, AddressMode::Zero, operand1),
0x75 => Instruction::two(Opcode::ADC, 4, AddressMode::ZeroX, operand1),
@ -209,7 +234,7 @@ impl Instruction {
0xf9 => Instruction::thr(Opcode::SBC, 4, AddressMode::AbsY, operand1, operand2),
0xe1 => Instruction::two(Opcode::SBC, 6, AddressMode::IndX, operand1),
0xf1 => Instruction::two(Opcode::SBC, 5, AddressMode::IndY, operand1),
0xa2 => Instruction::two(Opcode::LDX, 2, AddressMode::Imm, operand1),
0xa6 => Instruction::two(Opcode::LDX, 3, AddressMode::Zero, operand1),
0xb6 => Instruction::two(Opcode::LDX, 4, AddressMode::ZeroY, operand1),
@ -241,13 +266,13 @@ impl Instruction {
0x39 => Instruction::thr(Opcode::AND, 4, AddressMode::AbsY, operand1, operand2),
0x21 => Instruction::two(Opcode::AND, 6, AddressMode::IndX, operand1),
0x31 => Instruction::two(Opcode::AND, 5, AddressMode::IndY, operand1),
0xa0 => Instruction::two(Opcode::LDY, 2, AddressMode::Imm, operand1),
0xa4 => Instruction::two(Opcode::LDY, 3, AddressMode::Zero, operand1),
0xb4 => Instruction::two(Opcode::LDY, 4, AddressMode::ZeroX, operand1),
0xac => Instruction::thr(Opcode::LDY, 4, AddressMode::Abs, operand1, operand2),
0xbc => Instruction::thr(Opcode::LDY, 4, AddressMode::AbsX, operand1, operand2),
0xa9 => Instruction::two(Opcode::LDA, 2, AddressMode::Imm, operand1),
0xa5 => Instruction::two(Opcode::LDA, 3, AddressMode::Zero, operand1),
0xb5 => Instruction::two(Opcode::LDA, 4, AddressMode::ZeroX, operand1),
@ -256,7 +281,7 @@ impl Instruction {
0xb9 => Instruction::thr(Opcode::LDA, 4, AddressMode::AbsY, operand1, operand2),
0xa1 => Instruction::two(Opcode::LDA, 6, AddressMode::IndX, operand1),
0xb1 => Instruction::two(Opcode::LDA, 5, AddressMode::IndY, operand1),
0x85 => Instruction::two(Opcode::STA, 3, AddressMode::Zero, operand1),
0x95 => Instruction::two(Opcode::STA, 4, AddressMode::ZeroX, operand1),
0x8d => Instruction::thr(Opcode::STA, 4, AddressMode::Abs, operand1, operand2),
@ -272,12 +297,12 @@ impl Instruction {
0x84 => Instruction::two(Opcode::STY, 3, AddressMode::Zero, operand1),
0x94 => Instruction::two(Opcode::STY, 4, AddressMode::ZeroX, operand1),
0x8c => Instruction::thr(Opcode::STY, 4, AddressMode::Abs, operand1, operand2),
0x4c => Instruction::thr(Opcode::JMP, 3, AddressMode::Abs, operand1, operand2),
0x6c => Instruction::thr(Opcode::JMP, 5, AddressMode::Ind, operand1, operand2),
0x20 => Instruction::thr(Opcode::JSR, 6, AddressMode::Abs, operand1, operand2),
0x60 => Instruction::imp(Opcode::RTS, 6),
0xd0 => Instruction::two(Opcode::BNE, 2, AddressMode::Rel, operand1),
0xf0 => Instruction::two(Opcode::BEQ, 2, AddressMode::Rel, operand1),
0x10 => Instruction::two(Opcode::BPL, 2, AddressMode::Rel, operand1),
@ -286,7 +311,7 @@ impl Instruction {
0x30 => Instruction::two(Opcode::BMI, 2, AddressMode::Rel, operand1),
0x50 => Instruction::two(Opcode::BVC, 2, AddressMode::Rel, operand1),
0x70 => Instruction::two(Opcode::BVS, 2, AddressMode::Rel, operand1),
0xca => Instruction::imp(Opcode::DEX, 2),
0x88 => Instruction::imp(Opcode::DEY, 2),
0xe8 => Instruction::imp(Opcode::INX, 2),
@ -301,38 +326,44 @@ impl Instruction {
0xf6 => Instruction::two(Opcode::INC, 6, AddressMode::ZeroX, operand1),
0xee => Instruction::thr(Opcode::INC, 6, AddressMode::Abs, operand1, operand2),
0xfe => Instruction::thr(Opcode::INC, 7, AddressMode::AbsX, operand1, operand2),
// 1 byte NOPs
0xea | 0x1a | 0x3a => Instruction::imp(Opcode::NOP, 2),
// 2 byte NOPs 2 cycles
0x80 | 0x82 | 0x89 | 0xc2 | 0xe2 => Instruction::two(Opcode::NOP, 2, AddressMode::Nop, operand1),
0x80 | 0x82 | 0x89 | 0xc2 | 0xe2 => {
Instruction::two(Opcode::NOP, 2, AddressMode::Nop, operand1)
}
// 2 byte NOPs 3 cycles
0x04 | 0x44 | 0x64 => Instruction::two(Opcode::NOP, 3, AddressMode::Nop, operand1),
// 2 byte NOPs 4 cycles
0x14 | 0x34 | 0x54 | 0x74 | 0xd4 | 0xf4 => Instruction::two(Opcode::NOP, 4, AddressMode::Nop, operand1),
0x14 | 0x34 | 0x54 | 0x74 | 0xd4 | 0xf4 => {
Instruction::two(Opcode::NOP, 4, AddressMode::Nop, operand1)
}
// 3 byte nop
0x0c => Instruction::thr(Opcode::NOP, 4, AddressMode::Nop, operand1, operand2),
// 3 byte NOPs, absX for cycle
0x1c | 0x3c | 0x5c | 0x7c | 0xdc | 0xfc => Instruction::thr(Opcode::NOP, 4, AddressMode::AbsX, operand1, operand2),
0x1c | 0x3c | 0x5c | 0x7c | 0xdc | 0xfc => {
Instruction::thr(Opcode::NOP, 4, AddressMode::AbsX, operand1, operand2)
}
0xc0 => Instruction::two(Opcode::CPY, 2, AddressMode::Imm, operand1),
0xc4 => Instruction::two(Opcode::CPY, 3, AddressMode::Zero, operand1),
0xcc => Instruction::thr(Opcode::CPY, 4, AddressMode::Abs, operand1, operand2),
0xe0 => Instruction::two(Opcode::CPX, 2, AddressMode::Imm, operand1),
0xe4 => Instruction::two(Opcode::CPX, 3, AddressMode::Zero, operand1),
0xec => Instruction::thr(Opcode::CPX, 4, AddressMode::Abs, operand1, operand2),
0xc9 => Instruction::two(Opcode::CMP, 2, AddressMode::Imm, operand1),
0xc5 => Instruction::two(Opcode::CMP, 3, AddressMode::Zero, operand1),
0xd5 => Instruction::two(Opcode::CMP, 4, AddressMode::ZeroX, operand1),
0xcd => Instruction::thr(Opcode::CMP, 4, AddressMode::Abs, operand1, operand2),
0xdd => Instruction::thr(Opcode::CMP, 4, AddressMode::AbsX, operand1, operand2),
0xd9 => Instruction::thr(Opcode::CMP, 4, AddressMode::AbsY, operand1, operand2),
0xd9 => Instruction::thr(Opcode::CMP, 4, AddressMode::AbsY, operand1, operand2),
0xc1 => Instruction::two(Opcode::CMP, 6, AddressMode::IndX, operand1),
0xd1 => Instruction::two(Opcode::CMP, 5, AddressMode::IndY, operand1),
0x4a => Instruction::imp(Opcode::LSR, 2),
0x46 => Instruction::two(Opcode::LSR, 5, AddressMode::Zero, operand1),
0x56 => Instruction::two(Opcode::LSR, 6, AddressMode::ZeroX, operand1),
@ -356,7 +387,7 @@ impl Instruction {
0x76 => Instruction::two(Opcode::ROR, 6, AddressMode::ZeroX, operand1),
0x6e => Instruction::thr(Opcode::ROR, 6, AddressMode::Abs, operand1, operand2),
0x7e => Instruction::thr(Opcode::ROR, 7, AddressMode::AbsX, operand1, operand2),
0x48 => Instruction::imp(Opcode::PHA, 3),
0x68 => Instruction::imp(Opcode::PLA, 4),
0x08 => Instruction::imp(Opcode::PHP, 3),
@ -438,8 +469,8 @@ impl Instruction {
0x2b => Instruction::two(Opcode::ANC2, 2, AddressMode::Imm, operand1),
0x4b => Instruction::two(Opcode::ALR, 2, AddressMode::Imm, operand1),
_ => panic!("Unknown opcode: {:#04x}", opbyte)
_ => panic!("Unknown opcode: {:#04x}", opbyte),
}
}
}
}

View file

@ -3,15 +3,15 @@
extern crate alloc;
mod address_mode;
mod instructions;
pub mod cpu;
#[cfg(feature = "debugger")]
pub mod debugger;
pub mod mos6502;
mod instructions;
pub mod memory;
pub mod mos6502;
#[cfg(not(feature = "debugger"))]
pub mod debugger {
#[derive(Default)]
pub struct Debugger;
}
}

View file

@ -1,7 +1,7 @@
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::ops::RangeInclusive;
use alloc::{vec::Vec, boxed::Box};
const MEM_SIZE: usize = 0xffff + 1;
pub trait Bus {
@ -33,4 +33,4 @@ impl Bus for Memory {
fn write8(&mut self, val: u8, address: u16) {
self.0[address as usize] = val;
}
}
}

View file

@ -1,6 +1,8 @@
use alloc::boxed::Box;
use crate::{cpu::Cpu, memory::Bus, debugger::Debugger};
use crate::cpu::Cpu;
use crate::debugger::Debugger;
use crate::memory::Bus;
pub struct Mos6502 {
cpu: Cpu,
@ -13,7 +15,12 @@ pub struct Mos6502 {
impl Mos6502 {
pub fn new(cpu: Cpu) -> Self {
let debugger = Debugger::default();
Self { cpu, debugger, total_cycles: 0, total_ticks: 0 }
Self {
cpu,
debugger,
total_cycles: 0,
total_ticks: 0,
}
}
pub fn cpu(&self) -> &Cpu {

View file

@ -1,13 +1,20 @@
use mos6502::{memory::Memory, cpu::Cpu, mos6502::Mos6502};
use mos6502::cpu::Cpu;
use mos6502::memory::Memory;
use mos6502::mos6502::Mos6502;
fn run_test_rom(file: &str, load_base: u16, entry_point: u16, success_address: u16) -> (bool, usize) {
fn run_test_rom(
file: &str,
load_base: u16,
entry_point: u16,
success_address: u16,
) -> (bool, usize) {
let path = format!("../test-roms/bin/{}", file);
let program = std::fs::read(path).expect("failed to load test rom");
let mem = Memory::load(&program[..], load_base);
let mut cpu = Cpu::new(mem);
cpu.set_pc(entry_point);
// debugger.add_breakpoint(Breakpoint::Opcode("DEX".into()));
let mut machine = Mos6502::new(cpu);
@ -23,19 +30,19 @@ fn run_test_rom(file: &str, load_base: u16, entry_point: u16, success_address: u
// Panic if looping on PC, most likely functional_tests trap.
if Some(pc) == last_pc {
machine.debugger().dump_backtrace();
return (false, ticks)
return (false, ticks);
}
// JMP start == catastrophic error
if last_pc.is_some() && pc == entry_point {
machine.debugger().dump_backtrace();
return (false, ticks)
return (false, ticks);
}
if pc == success_address {
// println!("✅ TEST SUCCESSFUL! Hit success address at {:#06x}", pc);
// machine.debugger().dump_fun_stats();
return (true, ticks)
return (true, ticks);
}
last_pc = Some(pc);
@ -74,4 +81,4 @@ fn functional_test_extended_opcodes() {
let res = run_test_rom("extended_test.bin", 0x000, 0x400, 0x336d);
assert!(res.0, "trapped");
assert_eq!(expected_ticks, res.1, "wrong tick count");
}
}

View file

@ -1,10 +1,15 @@
use std::{collections::HashSet, time::Instant};
use jni::{
objects::{JClass, JObject, GlobalRef},
sys::{jlong, jbyteArray},
JNIEnv,
};
use nes::{cartridge::Cartridge, nes::Nes, joypad::JoypadButton};
use std::collections::HashSet;
use std::time::Instant;
use jni::objects::GlobalRef;
use jni::objects::JClass;
use jni::objects::JObject;
use jni::sys::jbyteArray;
use jni::sys::jlong;
use jni::JNIEnv;
use nes::cartridge::Cartridge;
use nes::joypad::JoypadButton;
use nes::nes::Nes;
static KEYS: &[JoypadButton] = &[
JoypadButton::B,
@ -14,36 +19,42 @@ static KEYS: &[JoypadButton] = &[
JoypadButton::LEFT,
JoypadButton::RIGHT,
JoypadButton::START,
JoypadButton::SELECT
JoypadButton::SELECT,
];
#[no_mangle]
pub extern "C" fn Java_nes_potatis_Rust_init(
env: JNIEnv<'static>,
_: JClass,
env: JNIEnv<'static>,
_: JClass,
rom: jbyteArray,
bindings: JObject,
panic_handler: JObject
panic_handler: JObject,
) -> jlong {
// Panic to LogCat
let jvm = env.get_java_vm().unwrap();
let panic_handler = env.new_global_ref(panic_handler).unwrap();
std::panic::set_hook(Box::new(move |info| {
let env = jvm.get_env().unwrap();
let s = env.new_string(info.to_string()).unwrap();
env.call_method(&panic_handler, "panic", "(Ljava/lang/String;)V", &[s.into()]).unwrap();
env
.call_method(
&panic_handler,
"panic",
"(Ljava/lang/String;)V",
&[s.into()],
)
.unwrap();
}));
// Create global refs to unsure JVM doesn't GC them
let bindings = env.new_global_ref(bindings).unwrap();
let rom = env.convert_byte_array(rom).unwrap();
let rom = if rom.is_empty() {
include_bytes!("../../test-roms/nestest/nestest.nes").to_vec()
} else {
rom
};
include_bytes!("../../test-roms/nestest/nestest.nes").to_vec()
} else {
rom
};
let cart = Cartridge::blow_dust_vec(rom).unwrap();
let host = AndroidHost::new(env, bindings);
let nes = Nes::insert(cart, host);
@ -76,7 +87,12 @@ struct AndroidHost {
impl AndroidHost {
fn new(env: JNIEnv<'static>, bindings: GlobalRef) -> Self {
Self { env, bindings, pressed: HashSet::with_capacity(8), time: Instant::now() }
Self {
env,
bindings,
pressed: HashSet::with_capacity(8),
time: Instant::now(),
}
}
}
@ -96,14 +112,20 @@ impl nes::nes::HostPlatform for AndroidHost {
let jobj = JObject::from_raw(jpixels);
// TODO: Is it possible/good for perf to cache the method lookup? call_method_unchecked.
self.env.call_method(&self.bindings, "render", "([B)V", &[jobj.into()]).unwrap();
self
.env
.call_method(&self.bindings, "render", "([B)V", &[jobj.into()])
.unwrap();
}
}
fn poll_events(&mut self, joypad: &mut nes::joypad::Joypad) -> nes::nes::Shutdown {
let state = self.env.call_method(&self.bindings, "input", "()B", &[]).unwrap();
let state = self
.env
.call_method(&self.bindings, "input", "()B", &[])
.unwrap();
let state = state.b().unwrap();
let was_pressed = self.pressed.clone();
self.pressed.clear();
for (i, k) in KEYS.iter().enumerate() {
@ -116,11 +138,12 @@ impl nes::nes::HostPlatform for AndroidHost {
joypad.on_event(nes::joypad::JoypadEvent::Press(*btn));
});
was_pressed.symmetric_difference(&self.pressed).for_each(|btn| {
joypad.on_event(nes::joypad::JoypadEvent::Release(*btn));
});
was_pressed
.symmetric_difference(&self.pressed)
.for_each(|btn| {
joypad.on_event(nes::joypad::JoypadEvent::Release(*btn));
});
nes::nes::Shutdown::No
}
}
}

View file

@ -33,4 +33,4 @@ impl Ansi<'_> {
let bgi = ansi_colours::ansi256_from_rgb(bg);
format!("\x1b[38;5;{}m\x1b[48;5;{}m{}", fgi, bgi, self.0)
}
}
}

View file

@ -1,8 +1,21 @@
use std::{sync::mpsc, collections::HashMap, time::{Instant, Duration}, io::Write};
use log::warn;
use nes::{joypad::{JoypadButton, Joypad, JoypadEvent}, frame::RenderFrame, nes::{Shutdown, HostPlatform}};
use std::collections::HashMap;
use std::io::Write;
use std::sync::mpsc;
use std::time::Duration;
use std::time::Instant;
use crate::{io::CloudStream, renderers::{Renderer, self, RenderMode}};
use log::warn;
use nes::frame::RenderFrame;
use nes::joypad::Joypad;
use nes::joypad::JoypadButton;
use nes::joypad::JoypadEvent;
use nes::nes::HostPlatform;
use nes::nes::Shutdown;
use crate::io::CloudStream;
use crate::renderers::RenderMode;
use crate::renderers::Renderer;
use crate::renderers::{self,};
const PRESS_RELEASED_AFTER_MS: u128 = 250;
@ -15,32 +28,34 @@ pub struct CloudHost {
crc: u32,
time: Instant,
transmitted: usize,
tx_b_limit: usize
tx_b_limit: usize,
}
impl CloudHost {
pub fn new(
stream: CloudStream,
rx: mpsc::Receiver<u8>,
stream: CloudStream,
rx: mpsc::Receiver<u8>,
mode: RenderMode,
tx_mb_limit: usize,
) -> Self {
let renderer = renderers::create(mode);
Self {
Self {
stream,
rx,
rx,
pressed: HashMap::new(),
dead: false,
renderer,
crc: 0,
time: Instant::now(),
transmitted: 0,
tx_b_limit: tx_mb_limit * 1000 * 1000
tx_b_limit: tx_mb_limit * 1000 * 1000,
}
}
fn release_keys(&mut self, joypad: &mut Joypad) {
let to_release: Vec<JoypadButton> = self.pressed.iter()
let to_release: Vec<JoypadButton> = self
.pressed
.iter()
.filter(|(_, &at)| at.elapsed().as_millis() >= PRESS_RELEASED_AFTER_MS)
.map(|(b, _)| *b)
.collect();
@ -63,11 +78,11 @@ impl CloudHost {
0x0a => Some(JoypadButton::START),
b'a' | b'A' => Some(JoypadButton::LEFT),
b's' | b'S' => Some(JoypadButton::DOWN),
b'w' | b'W'=> Some(JoypadButton::UP),
b'w' | b'W' => Some(JoypadButton::UP),
b'd' | b'D' => Some(JoypadButton::RIGHT),
_ => None
_ => None,
}
}
}
}
impl HostPlatform for CloudHost {
@ -90,12 +105,12 @@ impl HostPlatform for CloudHost {
match self.rx.recv_timeout(Duration::from_millis(0)) {
Ok(key) => {
let button = self.map_button(key);
if let Some(joypad_btn) = button {
*self.pressed.entry(joypad_btn).or_insert_with(Instant::now) = Instant::now();
joypad.on_event(JoypadEvent::Press(joypad_btn));
}
},
}
Err(mpsc::RecvTimeoutError::Disconnected) => self.dead = true,
Err(mpsc::RecvTimeoutError::Timeout) => {
self.release_keys(joypad);
@ -111,4 +126,4 @@ impl HostPlatform for CloudHost {
fn delay(&self, d: Duration) {
std::thread::sleep(d);
}
}
}

View file

@ -1,6 +1,10 @@
use std::{io::{Write, Read, Cursor}, net::TcpStream, time::Duration};
use std::io::Cursor;
use std::io::Read;
use std::io::Write;
use std::net::TcpStream;
use std::time::Duration;
pub enum CloudStream {
pub enum CloudStream {
Offline,
Online(TcpStream),
}
@ -37,4 +41,4 @@ impl Read for CloudStream {
CloudStream::Online(socket) => socket.read(buf),
}
}
}
}

View file

@ -1,18 +1,43 @@
#![feature(iter_array_chunks)]
use std::{error::Error, net::TcpStream, io::{Write, Read}, os::unix::prelude::FromRawFd, ops::Sub, path::PathBuf, fmt::{Display}, time::Duration, sync::mpsc::{Sender, Receiver}, ptr::read};
use log::{info, warn, debug, error};
use nes::{cartridge::{Cartridge, Header, HeapRom, error::CartridgeError}, nes::Nes};
use std::error::Error;
use std::fmt::Display;
use std::io::Read;
use std::io::Write;
use std::net::TcpStream;
use std::ops::Sub;
use std::os::unix::prelude::FromRawFd;
use std::path::PathBuf;
use std::ptr::read;
use std::sync::mpsc::Receiver;
use std::sync::mpsc::Sender;
use std::time::Duration;
use libcloud::logging;
use libcloud::resources::Resources;
use libcloud::resources::StrId;
use libcloud::utils::strhash;
use libcloud::utils::ReadByte;
use libcloud::ServerMode;
use libcloud::{self,};
use log::debug;
use log::error;
use log::info;
use log::warn;
use nes::cartridge::error::CartridgeError;
use nes::cartridge::Cartridge;
use nes::cartridge::Header;
use nes::cartridge::HeapRom;
use nes::nes::Nes;
use renderers::RenderMode;
use crate::{io::CloudStream, host::CloudHost};
use crate::host::CloudHost;
use crate::io::CloudStream;
use libcloud::{self, logging, resources::{StrId, Resources}, ServerMode, utils::{ReadByte, strhash}};
mod renderers;
mod io;
mod ansi;
mod host;
mod io;
mod renderers;
const FD_STDOUT: i32 = 1;
@ -20,19 +45,19 @@ const FD_STDOUT: i32 = 1;
enum RomSelection {
Invalid(char),
Included(PathBuf),
Cart(Cartridge<HeapRom>, md5::Digest)
Cart(Cartridge<HeapRom>, md5::Digest),
}
#[derive(Debug)]
struct InstanceError<S : AsRef<str>>(S);
struct InstanceError<S: AsRef<str>>(S);
impl<S : AsRef<str>> Display for InstanceError<S> {
impl<S: AsRef<str>> Display for InstanceError<S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.as_ref())
}
}
impl<S : AsRef<str> + std::fmt::Debug> std::error::Error for InstanceError<S> {}
impl<S: AsRef<str> + std::fmt::Debug> std::error::Error for InstanceError<S> {}
fn pipe_or_select_rom(r: &mut impl Read, res: &Resources) -> Result<RomSelection, Box<dyn Error>> {
let byte = r.read_byte()?;
@ -44,8 +69,11 @@ fn pipe_or_select_rom(r: &mut impl Read, res: &Resources) -> Result<RomSelection
if let Some(selection) = (byte as char).to_digit(10) {
let roms_included = res.included_roms();
if selection > 0 && selection <= roms_included.len() as u32 {
let path: PathBuf = roms_included.get(selection.sub(1) as usize).unwrap().to_path_buf();
return Ok(RomSelection::Included(path))
let path: PathBuf = roms_included
.get(selection.sub(1) as usize)
.unwrap()
.to_path_buf();
return Ok(RomSelection::Included(path));
}
}
@ -57,7 +85,7 @@ fn read_rom(r: &mut impl Read) -> Result<RomSelection, Box<dyn Error>> {
r.read_exact(&mut rest_of_magic)?;
if rest_of_magic != nes::cartridge::MAGIC[1..] {
return Err(Box::new(CartridgeError::InvalidCartridge("magic")))
return Err(Box::new(CartridgeError::InvalidCartridge("magic")));
}
let mut header_buf = [0u8; 16];
@ -86,7 +114,12 @@ fn select_render_mode(stream: &mut impl Read) -> Result<RenderMode, Box<dyn Erro
b'2' => Ok(RenderMode::Color),
b'3' => Ok(RenderMode::Ascii),
0x0a if first => prompt(stream, false),
_ => return Err(Box::new(InstanceError(format!("Invalid render selection: {:#04x}", input))))
_ => {
return Err(Box::new(InstanceError(format!(
"Invalid render selection: {:#04x}",
input
))))
}
}
}
@ -106,9 +139,9 @@ fn recv_thread(mut stream: CloudStream, tx: Sender<u8>) {
}
fn emulation_thread(
stream: CloudStream,
rx: Receiver<u8>,
cart: Cartridge<HeapRom>,
stream: CloudStream,
rx: Receiver<u8>,
cart: Cartridge<HeapRom>,
mode: RenderMode,
res: &Resources,
) {
@ -118,7 +151,11 @@ fn emulation_thread(
RenderMode::Sixel => res.fps_conf().sixel,
};
info!("Starting emulation. FPS: {}, limit: {}", fps, res.tx_mb_limit());
info!(
"Starting emulation. FPS: {}, limit: {}",
fps,
res.tx_mb_limit()
);
let host = CloudHost::new(stream, rx, mode, res.tx_mb_limit());
let mut nes = Nes::insert(cart, host);
@ -132,8 +169,7 @@ fn emulation_thread(
}
fn main() -> Result<(), Box<dyn Error>> {
logging::init(std::env::var("LOG_TO_FILE")
.map_or(false, |s| s.parse().unwrap_or(false)))?;
logging::init(std::env::var("LOG_TO_FILE").map_or(false, |s| s.parse().unwrap_or(false)))?;
let oghook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
@ -143,8 +179,8 @@ fn main() -> Result<(), Box<dyn Error>> {
}));
let fd = std::env::var("FD");
let srv_mode: ServerMode = std::env::var("MODE")
.map_or(ServerMode::User, |s| s.parse().unwrap_or(ServerMode::User));
let srv_mode: ServerMode =
std::env::var("MODE").map_or(ServerMode::User, |s| s.parse().unwrap_or(ServerMode::User));
info!("Instance started. FD: {:?}, Mode: {:?}", fd, srv_mode);
let mut res = Resources::load("resources.yaml");
@ -152,9 +188,9 @@ fn main() -> Result<(), Box<dyn Error>> {
let mut stream: CloudStream = match fd?.parse() {
Ok(FD_STDOUT) => CloudStream::Offline,
Ok(socketfd) => unsafe { CloudStream::Online(TcpStream::from_raw_fd(socketfd)) },
Err(e) => panic!("invalid FD: {}", e)
Err(e) => panic!("invalid FD: {}", e),
};
// Say hello
let players = std::env::var("PLAYERS").unwrap_or_else(|_| "0".into());
stream.write_all(&res.fmt(StrId::Welcome, &[&players]))?;
@ -172,23 +208,21 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(cart) => cart,
Err(e) => panic!("Failed to load included ROM: {}", e),
}
},
}
Ok(RomSelection::Cart(cart, hash)) => {
stream.write_all(&res.fmt(
StrId::RomInserted,
&[&cart.to_string(), &strhash(&hash)]
)).unwrap();
stream
.write_all(&res.fmt(StrId::RomInserted, &[&cart.to_string(), &strhash(&hash)]))
.unwrap();
cart
}
Ok(RomSelection::Invalid(_)) => {
stream.write_all(&res[StrId::InvalidRomSelection]).unwrap();
return Err(Box::new(InstanceError("Invalid ROM selection.")));
},
}
Err(e) if e.is::<CartridgeError>() => {
stream.write_all(&res.fmt(
StrId::InvalidRom,
&[&e.to_string()])
).unwrap();
stream
.write_all(&res.fmt(StrId::InvalidRom, &[&e.to_string()]))
.unwrap();
return Err(e);
}
Err(e) => {
@ -223,8 +257,8 @@ fn main() -> Result<(), Box<dyn Error>> {
let (tx, rx) = std::sync::mpsc::channel::<u8>();
std::thread::scope(|scope| {
let s = stream.clone();
scope.spawn(|| { recv_thread(s, tx) });
scope.spawn(|| { emulation_thread(stream, rx, cart, mode, &res) });
scope.spawn(|| recv_thread(s, tx));
scope.spawn(|| emulation_thread(stream, rx, cart, mode, &res));
if std::env::var("PANIC").is_ok() {
std::thread::sleep(Duration::from_millis(1000));
@ -237,13 +271,16 @@ fn main() -> Result<(), Box<dyn Error>> {
Ok(())
}
#[cfg(test)]
mod tests {
use std::{io::Cursor, error::Error};
use libcloud::{resources::Resources, utils::strhash};
use std::error::Error;
use std::io::Cursor;
use crate::{RomSelection, pipe_or_select_rom};
use libcloud::resources::Resources;
use libcloud::utils::strhash;
use crate::pipe_or_select_rom;
use crate::RomSelection;
impl PartialEq for RomSelection {
fn eq(&self, other: &Self) -> bool {
@ -284,7 +321,10 @@ mod tests {
let mapper7 = include_bytes!("../../../test-roms/nes-test-roms/other/oam3.nes");
let result = input(mapper7);
assert!(result.is_err());
assert_eq!(result.err().unwrap().to_string(), "NotYetImplemented(\"Mapper 7\")")
assert_eq!(
result.err().unwrap().to_string(),
"NotYetImplemented(\"Mapper 7\")"
)
}
#[test]
@ -294,16 +334,24 @@ mod tests {
Ok(RomSelection::Cart(cart, hash)) => {
assert_eq!(cart.to_string(), expected);
assert_eq!(exphash, strhash(&hash));
},
Ok(_) => panic!("invalid response") ,
}
Ok(_) => panic!("invalid response"),
Err(e) => panic!("{}", e),
}
}
let pm = include_bytes!("../../../test-roms/nestest/nestest.nes");
assert_pipe_rom(pm, "[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K", "4068f00f3db2fe783e437681fa6b419a");
assert_pipe_rom(
pm,
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K",
"4068f00f3db2fe783e437681fa6b419a",
);
let pm = include_bytes!("../../../test-roms/nes-test-roms/instr_misc/instr_misc.nes");
assert_pipe_rom(pm, "[Ines] Mapper: Mmc1, Mirroring: Vertical, CHR RAM: 1x8K, PRG: 4x16K", "df401ddc57943c774a225e9fb9b305a0");
assert_pipe_rom(
pm,
"[Ines] Mapper: Mmc1, Mirroring: Vertical, CHR RAM: 1x8K, PRG: 4x16K",
"df401ddc57943c774a225e9fb9b305a0",
);
}
}
}

View file

@ -1,12 +1,18 @@
use std::{fs::File, io::{Read, BufWriter}};
use nes::frame::{RenderFrame, PixelFormat, PixelFormatRGB888};
use std::fs::File;
use std::io::BufWriter;
use std::io::Read;
use crate::ansi::{Ansi, self};
use nes::frame::PixelFormat;
use nes::frame::PixelFormatRGB888;
use nes::frame::RenderFrame;
use crate::ansi::Ansi;
use crate::ansi::{self,};
const UPPER_BLOCK: &str = "\u{2580}";
#[derive(Debug, Clone, Copy)]
pub enum RenderMode {
pub enum RenderMode {
Color,
Ascii,
Sixel,
@ -44,16 +50,17 @@ struct SixelRenderer {
impl SixelRenderer {
pub fn new() -> Self {
let outfile = tempfile::Builder::new()
.prefix("sixel")
.tempfile()
.unwrap();
let outfile = tempfile::Builder::new().prefix("sixel").tempfile().unwrap();
let sixel = sixel_rs::encoder::Encoder::new().unwrap();
sixel.set_quality(sixel_rs::optflags::Quality::Low).unwrap();
sixel.set_output(outfile.path()).unwrap();
sixel.set_height(sixel_rs::optflags::SizeSpecification::Percent(300)).unwrap();
sixel.set_width(sixel_rs::optflags::SizeSpecification::Percent(300)).unwrap();
sixel
.set_height(sixel_rs::optflags::SizeSpecification::Percent(300))
.unwrap();
sixel
.set_width(sixel_rs::optflags::SizeSpecification::Percent(300))
.unwrap();
Self {
sixel,
@ -67,17 +74,14 @@ impl Renderer for SixelRenderer {
self.buf.set_len(0).unwrap();
// TODO: Avoid created a new file here. Reuse old tmp.
let infile = tempfile::Builder::new()
.prefix("frame")
.tempfile()
.unwrap();
let infile = tempfile::Builder::new().prefix("frame").tempfile().unwrap();
let inpath = infile.path().to_owned();
let w = &mut BufWriter::new(infile);
let mut png = png::Encoder::new(
w,
nes::frame::NTSC_WIDTH as u32,
nes::frame::NTSC_HEIGHT as u32
w,
nes::frame::NTSC_WIDTH as u32,
nes::frame::NTSC_HEIGHT as u32,
);
png.set_color(png::ColorType::Rgb);
png.set_depth(png::BitDepth::Eight);
@ -85,7 +89,7 @@ impl Renderer for SixelRenderer {
let pixels: Vec<u8> = frame.pixels_ntsc().collect();
writer.write_image_data(&pixels).unwrap();
writer.finish().unwrap();
self.sixel.encode_file(&inpath).unwrap();
let mut buf = ansi::CURSOR_HOME_BYTES.to_vec();
@ -95,7 +99,7 @@ impl Renderer for SixelRenderer {
}
struct UnicodeColorRenderer {
buf: String
buf: String,
}
impl UnicodeColorRenderer {
@ -103,7 +107,9 @@ impl UnicodeColorRenderer {
const ROWS: usize = nes::frame::NTSC_HEIGHT;
fn new() -> Self {
UnicodeColorRenderer { buf: String::with_capacity(160000) }
UnicodeColorRenderer {
buf: String::with_capacity(160000),
}
}
}
@ -135,7 +141,7 @@ impl Renderer for UnicodeColorRenderer {
self.buf.push_str(UPPER_BLOCK);
}
self.buf.push('\n')
}
@ -144,7 +150,7 @@ impl Renderer for UnicodeColorRenderer {
}
struct AsciiRenderer {
buf: String
buf: String,
}
impl AsciiRenderer {
@ -152,7 +158,9 @@ impl AsciiRenderer {
const MAX: f64 = Self::CHARSET.len() as f64;
fn new() -> Self {
Self { buf: String::with_capacity(50000) }
Self {
buf: String::with_capacity(50000),
}
}
}
@ -161,12 +169,14 @@ impl Renderer for AsciiRenderer {
self.buf.clear();
self.buf.push_str(crate::ansi::CURSOR_HOME);
frame.pixels_ntsc()
.array_chunks::<{nes::frame::PixelFormatRGB888::BYTES_PER_PIXEL}>()
frame
.pixels_ntsc()
.array_chunks::<{ nes::frame::PixelFormatRGB888::BYTES_PER_PIXEL }>()
.enumerate()
.for_each(|(n, p)| {
// https://stackoverflow.com/questions/596216/formula-to-determine-perceived-brightness-of-rgb-color
let g: f64 = ((0.2126 * p[0] as f64) + (0.7152 * p[1] as f64) + (0.0722 * p[2] as f64)) / 255.0;
let g: f64 =
((0.2126 * p[0] as f64) + (0.7152 * p[1] as f64) + (0.0722 * p[2] as f64)) / 255.0;
let i = ((Self::MAX * g) + 0.5).floor();
let c = Self::CHARSET.chars().nth(i as usize).unwrap_or('.');
self.buf.push(c);
@ -182,10 +192,13 @@ impl Renderer for AsciiRenderer {
#[cfg(test)]
mod tests {
use nes::frame::{RenderFrame, PixelFormatRGB888};
use nes::frame::PixelFormatRGB888;
use nes::frame::RenderFrame;
use crate::renderers::{UnicodeColorRenderer, SixelRenderer};
use super::{AsciiRenderer, Renderer};
use super::AsciiRenderer;
use super::Renderer;
use crate::renderers::SixelRenderer;
use crate::renderers::UnicodeColorRenderer;
#[test]
fn frame_sizes() {
@ -210,4 +223,4 @@ mod tests {
// assert!(5 <= sixel565, "sixel 565 too big: {sixel565}kb");
}
}
}

View file

@ -1,11 +1,12 @@
use std::{error::Error};
use std::error::Error;
use libcloud::{logging, resources::Resources};
use libcloud::logging;
use libcloud::resources::Resources;
use server::Server;
use structopt::StructOpt;
mod server;
mod runners;
mod server;
#[derive(StructOpt, Debug)]
pub struct AppSettings {
@ -41,4 +42,4 @@ fn main() -> Result<(), Box<dyn Error>> {
Server::new(resources, settings).serve()?;
Ok(())
}
}

View file

@ -1,11 +1,12 @@
use crate::{server::Client, AppSettings};
use super::InstanceRunner;
use crate::server::Client;
use crate::AppSettings;
struct DockerInstanceRunner;
impl InstanceRunner for DockerInstanceRunner {
fn run(
&mut self,
&mut self,
_client: Client,
_tx: std::sync::mpsc::Sender<crate::server::Event>,
_settings: &AppSettings,
@ -13,4 +14,4 @@ impl InstanceRunner for DockerInstanceRunner {
) -> Result<(), Box<dyn std::error::Error>> {
todo!()
}
}
}

View file

@ -1,4 +1,5 @@
use std::{net::TcpStream, os::fd::AsRawFd};
use std::net::TcpStream;
use std::os::fd::AsRawFd;
const F_GETFD: i32 = 1;
const F_SETFD: i32 = 2;
@ -16,4 +17,4 @@ pub fn unset_fd_cloexec(s: &TcpStream) {
flags &= !FD_CLOEXEC;
fcntl(fd, F_SETFD, flags);
}
}
}

View file

@ -1,13 +1,16 @@
use std::{sync::mpsc::Sender};
use crate::{server::{Event, Client}, AppSettings};
use std::sync::mpsc::Sender;
use crate::server::Client;
use crate::server::Event;
use crate::AppSettings;
pub mod docker;
mod fcntl;
pub mod process;
pub mod docker;
pub trait InstanceRunner {
fn run(
&mut self,
&mut self,
client: Client,
tx: Sender<Event>,
settings: &AppSettings,

View file

@ -1,13 +1,19 @@
use std::{path::PathBuf, sync::mpsc::Sender, error::Error, os::fd::AsRawFd, process::Command};
use std::error::Error;
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::process::Command;
use std::sync::mpsc::Sender;
use log::info;
use crate::{server::{Event, Client}, runners::fcntl, AppSettings};
use super::InstanceRunner;
use crate::runners::fcntl;
use crate::server::Client;
use crate::server::Event;
use crate::AppSettings;
pub struct ProcessInstanceRunner {
child_binary_path: PathBuf
child_binary_path: PathBuf,
}
impl ProcessInstanceRunner {
@ -25,8 +31,8 @@ impl ProcessInstanceRunner {
impl InstanceRunner for ProcessInstanceRunner {
fn run(
&mut self,
client: Client,
tx: Sender<Event>,
client: Client,
tx: Sender<Event>,
settings: &AppSettings,
current_players: usize,
) -> Result<(), Box<dyn Error>> {
@ -41,7 +47,11 @@ impl InstanceRunner for ProcessInstanceRunner {
.env("PLAYERS", current_players.to_string())
.spawn()?;
info!("Spawned instance for fd: {}, pid: {}", socket_fd, child.id());
info!(
"Spawned instance for fd: {}, pid: {}",
socket_fd,
child.id()
);
std::thread::spawn(move || {
let code = child.wait();
@ -51,4 +61,4 @@ impl InstanceRunner for ProcessInstanceRunner {
Ok(())
}
}
}

View file

@ -1,9 +1,21 @@
use std::{error::Error, net::{TcpListener, TcpStream, SocketAddr}, io::Write, time::Duration};
use libcloud::{resources::{Resources, StrId}, ServerMode};
use std::error::Error;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpListener;
use std::net::TcpStream;
use std::sync::mpsc::Sender;
use log::{info, error, warn};
use std::time::Duration;
use crate::{AppSettings, runners::{process::ProcessInstanceRunner, InstanceRunner}};
use libcloud::resources::Resources;
use libcloud::resources::StrId;
use libcloud::ServerMode;
use log::error;
use log::info;
use log::warn;
use crate::runners::process::ProcessInstanceRunner;
use crate::runners::InstanceRunner;
use crate::AppSettings;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClientId(SocketAddr);
@ -20,7 +32,7 @@ pub enum Event {
Error(String),
Connect(Client),
Disconnect(ClientId),
Blocked(Client, Vec<u8>)
Blocked(Client, Vec<u8>),
}
pub struct Server {
@ -32,7 +44,7 @@ pub struct Server {
impl Server {
pub fn new(res: Resources, settings: AppSettings) -> Self {
Self {
Self {
res,
connected: Vec::with_capacity(settings.max_concurrent),
crd_timeout: Duration::from_millis(settings.client_read_timeout),
@ -54,8 +66,8 @@ impl Server {
info!("{:?} listening on {}", mode, host);
let server = TcpListener::bind(host)?;
Self::start_accepting(server, tx.clone(), mode);
};
}
// TODO: Inject?
let mut runner = ProcessInstanceRunner::new(&self.settings.instance_bin);
@ -64,7 +76,7 @@ impl Server {
match ev {
Event::Connect(client) => self.client_connected(&mut runner, client, tx.clone()),
Event::Disconnect(client_id) => self.client_disconnected(client_id),
Event::Blocked(mut client, msg) => { _ = client.socket.write_all(&msg) },
Event::Blocked(mut client, msg) => _ = client.socket.write_all(&msg),
Event::Error(e) => error!("Error: {}", e),
}
}
@ -78,26 +90,48 @@ impl Server {
loop {
match srv_socket.accept() {
Ok((socket, addr)) => {
let client = Client { id: ClientId(addr), socket, mode };
let client = Client {
id: ClientId(addr),
socket,
mode,
};
tx.send(Event::Connect(client))
}
Err(e) => tx.send(Event::Error(e.to_string())),
}.unwrap(); // Err == main thread died.
}
.unwrap(); // Err == main thread died.
}
});
}
fn client_disconnected(&mut self, client_id: ClientId) {
self.connected.retain(|c| c.0.ip() != client_id.0.ip());
error!("Client disconnected: {:?} ({} connected)", client_id, self.connected.len());
error!(
"Client disconnected: {:?} ({} connected)",
client_id,
self.connected.len()
);
}
fn client_connected(&mut self, runner: &mut ProcessInstanceRunner, client: Client, tx: Sender<Event>) {
info!("Client soft-connect: {:?} ({} connected)", client.id, self.connected.len());
fn client_connected(
&mut self,
runner: &mut ProcessInstanceRunner,
client: Client,
tx: Sender<Event>,
) {
info!(
"Client soft-connect: {:?} ({} connected)",
client.id,
self.connected.len()
);
// Block, with events
if let Some(msg) = self.block_client(&client.id) {
warn!("{:?} blocked: {}", client.id, String::from_utf8(msg.to_vec()).unwrap());
warn!(
"{:?} blocked: {}",
client.id,
String::from_utf8(msg.to_vec()).unwrap()
);
tx.send(Event::Blocked(client, msg.to_vec())).unwrap();
return;
}
@ -111,8 +145,7 @@ impl Server {
if let Err(e) = runner.run(client, tx, &self.settings, self.connected.len()) {
// TODO: Event?
warn!("Runner failed to start: {}", e);
}
else {
} else {
info!("Client connected! {:?}", client_id);
self.connected.push(client_id)
}
@ -120,12 +153,11 @@ impl Server {
fn block_client(&self, client_id: &ClientId) -> Option<&[u8]> {
if self.connected.len() >= self.settings.max_concurrent {
return Some(&self.res[StrId::TooManyPlayers])
return Some(&self.res[StrId::TooManyPlayers]);
}
if self.settings.block_dup &&
self.connected.iter().any(|&c| c.0.ip() == client_id.0.ip()) {
return Some(&self.res[StrId::AlreadyConnected])
if self.settings.block_dup && self.connected.iter().any(|&c| c.0.ip() == client_id.0.ip()) {
return Some(&self.res[StrId::AlreadyConnected]);
}
None
}
}
}

View file

@ -1,7 +1,8 @@
use std::{fmt::Display, str::FromStr};
use std::fmt::Display;
use std::str::FromStr;
pub mod resources;
pub mod logging;
pub mod resources;
#[derive(Debug, Clone, Copy)]
pub enum ServerMode {
@ -26,7 +27,7 @@ impl FromStr for ServerMode {
"Color" => Ok(Self::Color),
"Sixel" => Ok(Self::Sixel),
"User" => Ok(Self::User),
_ => Err(())
_ => Err(()),
}
}
}
@ -38,7 +39,10 @@ pub mod utils {
fn read_byte(&mut self) -> Result<u8, std::io::Error>;
}
impl<R> ReadByte for R where R : Read {
impl<R> ReadByte for R
where
R: Read,
{
fn read_byte(&mut self) -> Result<u8, std::io::Error> {
let mut buf = [0; 1];
self.read_exact(&mut buf)?;
@ -49,4 +53,4 @@ pub mod utils {
pub fn strhash(digest: &md5::Digest) -> String {
format!("{:x}", digest)
}
}
}

View file

@ -1,6 +1,13 @@
use std::error::Error;
use flexi_logger::{DeferredNow, Record, style, TS_DASHES_BLANK_COLONS_DOT_BLANK, Logger, Duplicate, FileSpec, WriteMode};
use flexi_logger::style;
use flexi_logger::DeferredNow;
use flexi_logger::Duplicate;
use flexi_logger::FileSpec;
use flexi_logger::Logger;
use flexi_logger::Record;
use flexi_logger::WriteMode;
use flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK;
fn log_format(
w: &mut dyn std::io::Write,
@ -9,14 +16,17 @@ fn log_format(
) -> Result<(), std::io::Error> {
let level = record.level();
write!(
w,
"[{}] {} [{}:{}] {} {}",
style(level).paint(now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK).to_string()),
style(level).paint(level.to_string()),
record.file().unwrap_or("<unnamed>"),
record.line().unwrap_or(0),
style(level).paint(format!("[fd: {}]", std::env::var("FD").unwrap_or_else(|_| "N/A".into()))),
style(level).paint(&record.args().to_string())
w,
"[{}] {} [{}:{}] {} {}",
style(level).paint(now.format(TS_DASHES_BLANK_COLONS_DOT_BLANK).to_string()),
style(level).paint(level.to_string()),
record.file().unwrap_or("<unnamed>"),
record.line().unwrap_or(0),
style(level).paint(format!(
"[fd: {}]",
std::env::var("FD").unwrap_or_else(|_| "N/A".into())
)),
style(level).paint(&record.args().to_string())
)
// Ok(())
}
@ -28,7 +38,8 @@ pub fn init(file: bool) -> Result<(), Box<dyn Error>> {
.duplicate_to_stdout(Duplicate::All);
if file {
logger.log_to_file(FileSpec::default())
logger
.log_to_file(FileSpec::default())
.write_mode(WriteMode::BufferAndFlush)
.start()?;
} else {
@ -36,4 +47,4 @@ pub fn init(file: bool) -> Result<(), Box<dyn Error>> {
}
Ok(())
}
}

View file

@ -1,4 +1,8 @@
use std::{fs, path::PathBuf, collections::HashMap, fmt::Debug};
use std::collections::HashMap;
use std::fmt::Debug;
use std::fs;
use std::path::PathBuf;
use log::debug;
use serde::Deserialize;
@ -35,7 +39,7 @@ impl Resources {
pub fn load(filepath: &str) -> Resources {
let f = match fs::File::open(filepath) {
Ok(f) => f,
Err(e) => panic!("could not open resource file ({}): {}", filepath, e)
Err(e) => panic!("could not open resource file ({}): {}", filepath, e),
};
let res: Resources = serde_yaml::from_reader(f).unwrap();
@ -88,14 +92,15 @@ impl std::ops::Index<StrId> for Resources {
#[cfg(test)]
mod tests {
use super::{Resources, StrId};
use super::Resources;
use super::StrId;
#[test]
fn res_fmt() {
let r = Resources::load("resources.yaml");
assert_eq!(
"\nYou have inserted a ROM:\nfoo\nbar\n",
"\nYou have inserted a ROM:\nfoo\nbar\n",
String::from_utf8(r.fmt(StrId::RomInserted, &["foo", "bar"])).unwrap()
);
}
}
}
}

View file

@ -1,10 +1,20 @@
#[macro_use]
extern crate lazy_static;
use std::{process::{Child, Stdio, ChildStderr}, net::{TcpStream, SocketAddr}, io::{Read, Write, BufReader, BufRead}, time::Duration};
use std::io::BufRead;
use std::io::BufReader;
use std::io::Read;
use std::io::Write;
use std::net::SocketAddr;
use std::net::TcpStream;
use std::process::Child;
use std::process::ChildStderr;
use std::process::Stdio;
use std::time::Duration;
use assert_cmd::prelude::CommandCargoExt;
use libcloud::{resources::{StrId, Resources}};
use libcloud::resources::Resources;
use libcloud::resources::StrId;
use rand::Rng;
const USER_PORT: u16 = 4444;
@ -55,8 +65,12 @@ impl Client {
let mut buf = vec![0; expected.len()];
match self.0.read_exact(&mut buf) {
Ok(()) => assert_eq_str(expected, &buf[0..expected.len()]),
Err(e) => panic!("assert_server_message: {}\nExpected: {:?}\n\nActual: {:?}\n",
e, String::from_utf8(expected.to_vec()), String::from_utf8(buf))
Err(e) => panic!(
"assert_server_message: {}\nExpected: {:?}\n\nActual: {:?}\n",
e,
String::from_utf8(expected.to_vec()),
String::from_utf8(buf)
),
}
}
@ -77,14 +91,16 @@ impl Client {
let mut buf = [0; 3];
match self.0.read_exact(&mut buf) {
Ok(_) => assert_eq!("\x1b[H".as_bytes(), &buf),
Err(e) => panic!("expect_frame: {}", e)
Err(e) => panic!("expect_frame: {}", e),
}
}
fn expect_disconnected(&mut self) {
let mut buf = [5u8; 1];
if let Ok(n) = self.0.read(&mut buf) {
if n != 0 { panic!("not disconnected") }
if n != 0 {
panic!("not disconnected")
}
}
}
@ -107,14 +123,19 @@ fn start_app() -> Result<SuicidalChild, Box<dyn std::error::Error>> {
start_app_with_settings(false, 5)
}
fn start_app_with_settings(block_dup: bool, max: usize) -> Result<SuicidalChild, Box<dyn std::error::Error>> {
fn start_app_with_settings(
block_dup: bool,
max: usize,
) -> Result<SuicidalChild, Box<dyn std::error::Error>> {
let mut cmd = std::process::Command::cargo_bin("nes-cloud-app")?;
if block_dup {
cmd.arg("--block-dup");
}
cmd.args([
"--max-concurrent", &max.to_string(),
"--client-read-timeout", "1500"
"--max-concurrent",
&max.to_string(),
"--client-read-timeout",
"1500",
]);
cmd.stderr(Stdio::piped());
let child = cmd.spawn()?;
@ -122,18 +143,20 @@ fn start_app_with_settings(block_dup: bool, max: usize) -> Result<SuicidalChild,
// Ready?
loop {
let stream = TcpStream::connect("127.0.0.1:4444");
if stream.is_ok() { break; }
if stream.is_ok() {
break;
}
}
std::thread::sleep(Duration::from_millis(500));
Ok(SuicidalChild(child))
}
#[test]
fn single_client() -> Result<(), Box<dyn std::error::Error>> {
fn single_client() -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app()?;
let mut client = Client::connect()?;
client.expect_welcome_and_rom_prompt();
@ -141,7 +164,7 @@ fn single_client() -> Result<(), Box<dyn std::error::Error>> {
}
#[test]
fn same_src_addr() -> Result<(), Box<dyn std::error::Error>> {
fn same_src_addr() -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app_with_settings(true, 5)?;
let mut client = Client::connect()?;
@ -159,7 +182,7 @@ fn same_src_addr() -> Result<(), Box<dyn std::error::Error>> {
}
#[test]
fn max_clients() -> Result<(), Box<dyn std::error::Error>> {
fn max_clients() -> Result<(), Box<dyn std::error::Error>> {
let max = 5;
let _child = start_app_with_settings(false, max)?;
@ -172,8 +195,7 @@ fn max_clients() -> Result<(), Box<dyn std::error::Error>> {
if i < max {
c.expect_server_message(&RES.fmt(StrId::Welcome, &[&i.to_string()]));
c.expect_server_message(&RES[StrId::RomSelection]);
}
else {
} else {
c.expect_server_message(&RES[StrId::TooManyPlayers]);
}
}
@ -187,12 +209,12 @@ fn max_clients() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
#[test]
fn select_valid_included_rom_invalid_render_mode_selection() -> Result<(), Box<dyn std::error::Error>> {
fn select_valid_included_rom_invalid_render_mode_selection(
) -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app()?;
let mut client = Client::connect()?;
client.expect_welcome_and_rom_prompt();
client.input(&[b'1']);
@ -203,7 +225,7 @@ fn select_valid_included_rom_invalid_render_mode_selection() -> Result<(), Box<d
// Invalid input
client.input(&[b'4']); // valid: 1-3
// Try again
// Try again
client.expect_server_message(&RES[StrId::InvalidRenderModeSelection]);
client.expect_disconnected();
@ -212,10 +234,11 @@ fn select_valid_included_rom_invalid_render_mode_selection() -> Result<(), Box<d
}
#[test]
fn select_valid_included_rom_valid_render_mode_selection() -> Result<(), Box<dyn std::error::Error>> {
fn select_valid_included_rom_valid_render_mode_selection() -> Result<(), Box<dyn std::error::Error>>
{
let _child = start_app()?;
let mut client = Client::connect()?;
client.expect_welcome_and_rom_prompt();
client.input(&[b'1']);
@ -234,10 +257,11 @@ fn select_valid_included_rom_valid_render_mode_selection() -> Result<(), Box<dyn
}
#[test]
fn select_valid_included_rom_invalid_render_mode_selection_icanon() -> Result<(), Box<dyn std::error::Error>> {
fn select_valid_included_rom_invalid_render_mode_selection_icanon(
) -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app()?;
let mut client = Client::connect()?;
client.expect_welcome_and_rom_prompt();
client.input(&[b'1']);
@ -253,10 +277,11 @@ fn select_valid_included_rom_invalid_render_mode_selection_icanon() -> Result<()
}
#[test]
fn select_valid_included_rom_valid_render_mode_selection_icanon() -> Result<(), Box<dyn std::error::Error>> {
fn select_valid_included_rom_valid_render_mode_selection_icanon(
) -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app()?;
let mut client = Client::connect()?;
client.expect_welcome_and_rom_prompt();
client.input(&[b'1']);
@ -289,17 +314,21 @@ fn select_invalid_included_rom() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn pipe_valid_rom() -> Result<(), Box<dyn std::error::Error>> {
let rom = include_bytes!("../../test-roms/nes-test-roms/cpu_dummy_writes/cpu_dummy_writes_ppumem.nes");
let rom =
include_bytes!("../../test-roms/nes-test-roms/cpu_dummy_writes/cpu_dummy_writes_ppumem.nes");
let _child = start_app()?;
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_server_message(&RES.fmt(
StrId::RomInserted,
&["[Ines] Mapper: Nrom, Mirroring: Vertical, CHR: 1x8K, PRG: 2x16K", "319a1ece57229c48663fec8bdf3764c0"]
StrId::RomInserted,
&[
"[Ines] Mapper: Nrom, Mirroring: Vertical, CHR: 1x8K, PRG: 2x16K",
"319a1ece57229c48663fec8bdf3764c0",
],
));
client.expect_render_mode_prompt();
@ -314,7 +343,8 @@ fn pipe_valid_rom() -> Result<(), Box<dyn std::error::Error>> {
}
#[test]
fn pipe_valid_rom_same_frame_same_crc_detect_disconnect() -> Result<(), Box<dyn std::error::Error>> {
fn pipe_valid_rom_same_frame_same_crc_detect_disconnect() -> Result<(), Box<dyn std::error::Error>>
{
// nestest has the same frame forever without any input
let rom = include_bytes!("../../test-roms/nestest/nestest.nes");
@ -322,13 +352,16 @@ fn pipe_valid_rom_same_frame_same_crc_detect_disconnect() -> Result<(), Box<dyn
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_server_message(&RES.fmt(
StrId::RomInserted,
&["[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K", "4068f00f3db2fe783e437681fa6b419a"]
StrId::RomInserted,
&[
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K",
"4068f00f3db2fe783e437681fa6b419a",
],
));
client.expect_render_mode_prompt();
client.input(&[b'3']);
@ -340,7 +373,8 @@ fn pipe_valid_rom_same_frame_same_crc_detect_disconnect() -> Result<(), Box<dyn
client.disconnect();
let stderr = child.premeditated_suicide();
stderr.lines()
stderr
.lines()
.map(|l| l.unwrap())
.find(|l| l.contains("Instance died."))
.expect("Emulation process did not die. Probably stuck in a same CRC loop.");
@ -356,12 +390,9 @@ fn pipe_invalid_rom() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_server_message(&RES.fmt(
StrId::InvalidRom,
&["InvalidCartridge(\"magic\")"]
));
client.expect_server_message(&RES.fmt(StrId::InvalidRom, &["InvalidCartridge(\"magic\")"]));
client.expect_disconnected();
Ok(())
@ -375,12 +406,9 @@ fn pipe_unsupported_rom() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_server_message(&RES.fmt(
StrId::InvalidRom,
&["NotYetImplemented(\"Mapper 7\")"]
));
client.expect_server_message(&RES.fmt(StrId::InvalidRom, &["NotYetImplemented(\"Mapper 7\")"]));
client.expect_disconnected();
Ok(())
@ -394,7 +422,7 @@ fn pipe_magic_then_timeout() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_disconnected();
@ -433,7 +461,7 @@ fn render_mode_color() -> Result<(), Box<dyn std::error::Error>> {
fn render_mode_sixel() -> Result<(), Box<dyn std::error::Error>> {
let _child = start_app()?;
let mut client = Client::connect_port(SIXEL_PORT)?;
client.expect_welcome_and_rom_prompt();
client.input(&[b'1']);
client.expect_press_any_key_to_boot("Sixel");
@ -445,7 +473,8 @@ fn render_mode_sixel() -> Result<(), Box<dyn std::error::Error>> {
#[test]
fn instance_panic_notify_client_and_close() -> Result<(), Box<dyn std::error::Error>> {
let rom = include_bytes!("../../test-roms/nes-test-roms/cpu_dummy_writes/cpu_dummy_writes_ppumem.nes");
let rom =
include_bytes!("../../test-roms/nes-test-roms/cpu_dummy_writes/cpu_dummy_writes_ppumem.nes");
std::env::set_var("PANIC", "1");
@ -453,11 +482,14 @@ fn instance_panic_notify_client_and_close() -> Result<(), Box<dyn std::error::Er
let mut client = Client::connect()?;
client.input(rom);
client.expect_welcome_and_rom_prompt();
client.expect_server_message(&RES.fmt(
StrId::RomInserted,
&["[Ines] Mapper: Nrom, Mirroring: Vertical, CHR: 1x8K, PRG: 2x16K", "319a1ece57229c48663fec8bdf3764c0"]
StrId::RomInserted,
&[
"[Ines] Mapper: Nrom, Mirroring: Vertical, CHR: 1x8K, PRG: 2x16K",
"319a1ece57229c48663fec8bdf3764c0",
],
));
client.expect_render_mode_prompt();
@ -474,12 +506,16 @@ fn instance_panic_notify_client_and_close() -> Result<(), Box<dyn std::error::Er
// Kill app process so we can read stderr until EOF
let stderr = child.premeditated_suicide();
let exp = format!("Client disconnected: ClientId({}) (0 connected)", client.0.local_addr().unwrap());
stderr.lines()
let exp = format!(
"Client disconnected: ClientId({}) (0 connected)",
client.0.local_addr().unwrap()
);
stderr
.lines()
.map(|l| l.unwrap())
.find(|l| l.contains(&exp))
.expect("Instance did not die on panic");
Ok(())
}
}

View file

@ -15,4 +15,4 @@ fn main() {
// Tell linker to look in target output
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
}
}

View file

@ -1,49 +1,56 @@
use rp2040_hal as hal;
use hal::pac as pac;
use embedded_hal::digital::v2::OutputPin;
use hal::gpio::{Pin, PushPullOutput, FunctionSpi};
use hal::gpio::bank0::{Gpio16, Gpio17};
use hal::sio::SioGpioBank0;
use hal::{Spi, spi::Enabled};
use hal::gpio::bank0::Gpio25;
use hal::gpio::PinId;
use fugit::RateExtU32;
use display_interface_spi::SPIInterface;
use embedded_hal::{blocking::delay::DelayUs, spi::MODE_0};
use embedded_hal::blocking::delay::DelayUs;
use embedded_hal::digital::v2::OutputPin;
use embedded_hal::spi::MODE_0;
use fugit::RateExtU32;
use hal::gpio::bank0::Gpio16;
use hal::gpio::bank0::Gpio17;
use hal::gpio::bank0::Gpio25;
use hal::gpio::FunctionSpi;
use hal::gpio::Pin;
use hal::gpio::PinId;
use hal::gpio::PushPullOutput;
use hal::pac;
use hal::sio::SioGpioBank0;
use hal::spi::Enabled;
use hal::Spi;
use rp2040_hal as hal;
use st7789::ST7789;
mod all_pins {
rp2040_hal::bsp_pins!(
Gpio16 {
name: lcd_dc,
aliases: { FunctionSpi: LcdDc }
},
Gpio17 {
name: lcd_cs,
aliases: { FunctionSpi: LcdCs }
},
Gpio18 {
name: spi_sclk,
aliases: { FunctionSpi: Sclk }
},
Gpio19 {
name: spi_mosi,
aliases: { FunctionSpi: Mosi }
},
Gpio25 { name: led },
Gpio16 {
name: lcd_dc,
aliases: { FunctionSpi: LcdDc }
},
Gpio17 {
name: lcd_cs,
aliases: { FunctionSpi: LcdCs }
},
Gpio18 {
name: spi_sclk,
aliases: { FunctionSpi: Sclk }
},
Gpio19 {
name: spi_mosi,
aliases: { FunctionSpi: Mosi }
},
Gpio25 { name: led },
);
}
pub type Screen = ST7789<
SPIInterface<Spi<Enabled, pac::SPI0, 8>, Pin<Gpio16, PushPullOutput>, Pin<Gpio17, PushPullOutput>>,
DummyPin,
DummyPin
SPIInterface<
Spi<Enabled, pac::SPI0, 8>,
Pin<Gpio16, PushPullOutput>,
Pin<Gpio17, PushPullOutput>,
>,
DummyPin,
DummyPin,
>;
pub struct Pins {
pub led: Pin<Gpio25, <Gpio25 as PinId>::Reset>
pub led: Pin<Gpio25, <Gpio25 as PinId>::Reset>,
}
pub struct Board {
@ -55,50 +62,39 @@ pub struct DummyPin;
impl OutputPin for DummyPin {
type Error = ();
fn set_high(&mut self) -> Result<(), Self::Error> {
Ok(())
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
Ok(())
Ok(())
}
}
impl Board {
pub fn new(
io: pac::IO_BANK0,
pads: pac::PADS_BANK0,
sio: SioGpioBank0,
spi0: pac::SPI0,
resets: &mut pac::RESETS,
delay: &mut impl DelayUs<u32>,
) -> (Self, Pins) {
let pins = all_pins::Pins::new(io, pads, sio, resets);
pub fn new(
io: pac::IO_BANK0,
pads: pac::PADS_BANK0,
sio: SioGpioBank0,
spi0: pac::SPI0,
resets: &mut pac::RESETS,
delay: &mut impl DelayUs<u32>,
) -> (Self, Pins) {
let pins = all_pins::Pins::new(io, pads, sio, resets);
let dc = pins.lcd_dc.into_push_pull_output();
let cs = pins.lcd_cs.into_push_pull_output();
let sck = pins.spi_sclk.into_mode::<FunctionSpi>();
let mosi = pins.spi_mosi.into_mode::<FunctionSpi>();
let dc = pins.lcd_dc.into_push_pull_output();
let cs = pins.lcd_cs.into_push_pull_output();
let sck = pins.spi_sclk.into_mode::<FunctionSpi>();
let mosi = pins.spi_mosi.into_mode::<FunctionSpi>();
let spi_screen = Spi::<_, _, 8>::new(spi0).init(
resets,
125u32.MHz(),
16u32.MHz(),
&MODE_0
);
let spi_screen = Spi::<_, _, 8>::new(spi0).init(resets, 125u32.MHz(), 16u32.MHz(), &MODE_0);
let spii_screen = SPIInterface::new(spi_screen, dc, cs);
let mut screen = ST7789::new(
spii_screen,
None,
None,
320,
240,
);
let spii_screen = SPIInterface::new(spi_screen, dc, cs);
let mut screen = ST7789::new(spii_screen, None, None, 320, 240);
screen.init(delay).unwrap();
screen
.set_orientation(st7789::Orientation::LandscapeSwapped)
.unwrap();
screen.init(delay).unwrap();
screen
.set_orientation(st7789::Orientation::LandscapeSwapped)
.unwrap();
(Self { screen }, Pins { led: pins.led })
}
}
(Self { screen }, Pins { led: pins.led })
}
}

View file

@ -1,9 +1,10 @@
use rp2040_hal as hal;
use fugit::RateExtU32;
use hal::{pac as pac, Watchdog, clocks::ClocksManager};
use hal::clocks::ClockSource;
use hal::clocks::ClocksManager;
use hal::pac;
use hal::Clock;
use hal::Watchdog;
use rp2040_hal as hal;
pub const XOSC_CRYSTAL_FREQ: u32 = 12_000_000;
@ -36,10 +37,7 @@ pub fn configure_overclock(
resets: &mut pac::RESETS,
watchdog: &mut Watchdog,
) -> ClocksManager {
let xosc = hal::xosc::setup_xosc_blocking(
xosc_dev,
XOSC_CRYSTAL_FREQ.Hz()
).unwrap();
let xosc = hal::xosc::setup_xosc_blocking(xosc_dev, XOSC_CRYSTAL_FREQ.Hz()).unwrap();
watchdog.enable_tick_generation((XOSC_CRYSTAL_FREQ / 1_000_000) as u8);
@ -76,4 +74,4 @@ pub fn configure_overclock(
clocks.init_default(&xosc, &pll_sys, &pll_usb).unwrap();
clocks
}
}

View file

@ -2,27 +2,36 @@
#![no_main]
#![feature(alloc_error_handler)]
use hal::multicore::Stack;
use hal::pac;
use hal::sio::SioFifo;
use hal::Sio;
use hal::Watchdog;
use rp2040_hal as hal;
use hal::{multicore::Stack, sio::SioFifo, pac, Sio, Watchdog};
mod clocks;
mod board;
mod clocks;
use core::alloc::Layout;
use core::cell::RefCell;
use board::Board;
use critical_section::Mutex;
use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use embedded_alloc::Heap;
use nes::{cartridge::Cartridge, nes::{Nes, HostPixelFormat}, frame::PixelFormat};
use core::{alloc::Layout, cell::RefCell};
use embedded_graphics::{
prelude::*,
pixelcolor::Rgb565, image::{ImageRaw, Image},
};
use embedded_graphics::image::Image;
use embedded_graphics::image::ImageRaw;
use embedded_graphics::pixelcolor::Rgb565;
use embedded_graphics::prelude::*;
use embedded_hal::digital::v2::OutputPin;
use hal::multicore::Multicore;
use hal::clocks::Clock;
use hal::multicore::Multicore;
use nes::cartridge::Cartridge;
use nes::frame::PixelFormat;
use nes::nes::HostPixelFormat;
use nes::nes::Nes;
use panic_probe as _;
#[link_section = ".boot_loader"]
#[used]
@ -31,8 +40,10 @@ pub static BOOT_LOADER: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
#[global_allocator]
static HEAP: Heap = Heap::empty();
const FRAME_BUF_SIZE: usize = nes::frame::NTSC_WIDTH * nes::frame::NTSC_HEIGHT * nes::frame::PixelFormatRGB565::BYTES_PER_PIXEL;
static FRAME_BUF: Mutex<RefCell<[u8; FRAME_BUF_SIZE]>> = Mutex::new(RefCell::new([0; FRAME_BUF_SIZE]));
const FRAME_BUF_SIZE: usize =
nes::frame::NTSC_WIDTH * nes::frame::NTSC_HEIGHT * nes::frame::PixelFormatRGB565::BYTES_PER_PIXEL;
static FRAME_BUF: Mutex<RefCell<[u8; FRAME_BUF_SIZE]>> =
Mutex::new(RefCell::new([0; FRAME_BUF_SIZE]));
static mut CORE1_STACK: Stack<2048> = Stack::new();
@ -50,7 +61,7 @@ impl EmbeddedHost {
fn new(core1: SioFifo) -> Self {
Self {
start: false,
core1
core1,
}
}
}
@ -67,7 +78,7 @@ impl nes::nes::HostPlatform for EmbeddedHost {
framebuf[i] = p;
}
});
self.core1.write(1);
}
@ -86,7 +97,7 @@ fn core1_render(sys_freq: u32) -> ! {
let (mut board, pins) = Board::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
sio.gpio_bank0,
pac.SPI0,
&mut pac.RESETS,
&mut delay,
@ -114,7 +125,7 @@ sio.gpio_bank0,
let image = Image::new(&raw_image, Point::zero());
image.draw(&mut board.screen).unwrap();
});
frame_n += 1;
}
}
@ -131,12 +142,12 @@ fn main() -> ! {
let mut pac = pac::Peripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = clocks::configure_overclock(
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
);
let mut sio = Sio::new(pac.SIO);
@ -147,16 +158,17 @@ fn main() -> ! {
info!("booting core1");
let sys_freq = clocks.system_clock.freq().to_Hz();
core1.spawn(unsafe { &mut CORE1_STACK.mem }, move || {
core1_render(sys_freq);
})
.expect("core1 failed");
core1
.spawn(unsafe { &mut CORE1_STACK.mem }, move || {
core1_render(sys_freq);
})
.expect("core1 failed");
let rom = include_bytes!(env!("ROM"));
let cart = Cartridge::blow_dust_no_heap(rom).unwrap();
let host = EmbeddedHost::new(sio.fifo);
let mut nes = Nes::insert(cart, host);
loop {
nes.tick();
}

View file

@ -1,7 +1,10 @@
use std::{path::PathBuf};
use nes::{cartridge::Cartridge, nes::Nes, mos6502::debugger::Breakpoint};
use structopt::StructOpt;
use std::path::PathBuf;
use common::utils;
use nes::cartridge::Cartridge;
use nes::mos6502::debugger::Breakpoint;
use nes::nes::Nes;
use structopt::StructOpt;
mod sdl;
use crate::sdl::SdlHostPlatform;
@ -16,7 +19,7 @@ struct Cli {
#[structopt(short, long)]
verbose: bool,
#[structopt(short, long)]
debug: bool
debug: bool,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -49,4 +52,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
Ok(())
}
}

View file

@ -1,7 +1,20 @@
use std::time::Instant;
use nes::{joypad::{Joypad, JoypadEvent, JoypadButton}, frame::{RenderFrame}, nes::{HostPlatform, Shutdown}};
use sdl2::{pixels::PixelFormatEnum, event::Event, keyboard::Keycode, Sdl, render::{Texture, Canvas, TextureCreator}, video::{Window, WindowContext}};
use nes::frame::RenderFrame;
use nes::joypad::Joypad;
use nes::joypad::JoypadButton;
use nes::joypad::JoypadEvent;
use nes::nes::HostPlatform;
use nes::nes::Shutdown;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::PixelFormatEnum;
use sdl2::render::Canvas;
use sdl2::render::Texture;
use sdl2::render::TextureCreator;
use sdl2::video::Window;
use sdl2::video::WindowContext;
use sdl2::Sdl;
pub struct SdlHostPlatform<'a> {
context: Sdl,
@ -21,15 +34,13 @@ impl SdlHostPlatform<'_> {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let window = video_subsystem.window("Potatis", w * scale, h * scale)
let window = video_subsystem
.window("Potatis", w * scale, h * scale)
.position_centered()
.build()
.unwrap();
let canvas = window.into_canvas()
.present_vsync()
.build()
.unwrap();
let canvas = window.into_canvas().present_vsync().build().unwrap();
let mut creator = canvas.texture_creator();
let texture: Texture = unsafe {
@ -38,7 +49,7 @@ impl SdlHostPlatform<'_> {
.create_texture_target(PixelFormatEnum::RGB24, w, h)
.unwrap()
};
Self {
_creator: creator,
context: sdl_context,
@ -52,24 +63,36 @@ impl SdlHostPlatform<'_> {
impl HostPlatform for SdlHostPlatform<'_> {
fn render(&mut self, frame: &RenderFrame) {
let pixels: Vec<u8> = frame.pixels_ntsc().collect();
self.texture.update(None, &pixels, frame.pitch_ntsc()).unwrap();
self
.texture
.update(None, &pixels, frame.pitch_ntsc())
.unwrap();
self.canvas.copy(&self.texture, None, None).unwrap();
self.canvas.present();
}
fn poll_events(&mut self, joypad: &mut Joypad, ) -> Shutdown {
fn poll_events(&mut self, joypad: &mut Joypad) -> Shutdown {
for event in self.context.event_pump().unwrap().poll_iter() {
if let Some(joypad_ev) = map_joypad(&event) {
joypad.on_event(joypad_ev);
continue;
}
match event {
Event::Quit {..} |
Event::KeyDown { keycode: Some(Keycode::Q), .. } |
Event::KeyDown { keycode: Some(Keycode::Escape), .. } => return Shutdown::Yes,
Event::KeyDown { keycode: Some(Keycode::R), .. } => return Shutdown::Reset,
_ => ()
Event::Quit { .. }
| Event::KeyDown {
keycode: Some(Keycode::Q),
..
}
| Event::KeyDown {
keycode: Some(Keycode::Escape),
..
} => return Shutdown::Yes,
Event::KeyDown {
keycode: Some(Keycode::R),
..
} => return Shutdown::Reset,
_ => (),
}
}
Shutdown::No
@ -87,13 +110,15 @@ impl HostPlatform for SdlHostPlatform<'_> {
fn map_joypad(sdlev: &Event) -> Option<JoypadEvent> {
match sdlev {
Event::KeyDown { keycode: Some(keycode), .. } => {
map_button(keycode).map(JoypadEvent::Press)
}
Event::KeyUp { keycode: Some(keycode), .. } => {
map_button(keycode).map(JoypadEvent::Release)
}
_ => None
Event::KeyDown {
keycode: Some(keycode),
..
} => map_button(keycode).map(JoypadEvent::Press),
Event::KeyUp {
keycode: Some(keycode),
..
} => map_button(keycode).map(JoypadEvent::Release),
_ => None,
}
}
@ -107,6 +132,6 @@ fn map_button(keycode: &Keycode) -> Option<JoypadButton> {
Keycode::L => Some(JoypadButton::A),
Keycode::Return => Some(JoypadButton::START),
Keycode::Space => Some(JoypadButton::SELECT),
_ => None
_ => None,
}
}

View file

@ -1,5 +1,13 @@
use nes::{cartridge::Cartridge, nes::{Nes, HostPlatform, Shutdown}, joypad::{JoypadButton, JoypadEvent}, frame::{PixelFormat, SetPixel}};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
use nes::cartridge::Cartridge;
use nes::frame::PixelFormat;
use nes::frame::SetPixel;
use nes::joypad::JoypadButton;
use nes::joypad::JoypadEvent;
use nes::nes::HostPlatform;
use nes::nes::Nes;
use nes::nes::Shutdown;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
pub struct PixelFormatRGBA8888;
@ -18,13 +26,18 @@ impl SetPixel for PixelFormatRGBA8888 {
#[wasm_bindgen]
#[derive(Debug, Clone, Copy, Default)]
pub enum KeyState { Pressed, Released, #[default] None }
pub enum KeyState {
Pressed,
Released,
#[default]
None,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct KeyboardState([KeyState; 8]);
#[wasm_bindgen]
extern {
extern "C" {
pub type BrowserNes;
#[wasm_bindgen(js_namespace = console)]
@ -50,7 +63,7 @@ pub fn main() -> Result<(), JsValue> {
struct WasmHostPlatform {
browser: BrowserNes,
keyboard: KeyboardState,
time: wasm_timer::Instant
time: wasm_timer::Instant,
}
impl HostPlatform for WasmHostPlatform {
@ -65,7 +78,9 @@ impl HostPlatform for WasmHostPlatform {
}
fn poll_events(&mut self, joypad: &mut nes::joypad::Joypad) -> Shutdown {
self.browser.poll_keyboard(self.keyboard.0.as_mut_ptr() as *mut u8);
self
.browser
.poll_keyboard(self.keyboard.0.as_mut_ptr() as *mut u8);
for (i, k) in self.keyboard.0.iter().enumerate() {
let button = match i {
@ -77,7 +92,7 @@ impl HostPlatform for WasmHostPlatform {
5 => JoypadButton::SELECT,
6 => JoypadButton::B,
7 => JoypadButton::A,
_ => continue
_ => continue,
};
let joypad_event = match k {
@ -103,7 +118,11 @@ impl HostPlatform for WasmHostPlatform {
impl WasmHostPlatform {
pub fn new(browser: BrowserNes) -> Self {
Self { browser, keyboard: KeyboardState::default(), time: wasm_timer::Instant::now() }
Self {
browser,
keyboard: KeyboardState::default(),
time: wasm_timer::Instant::now(),
}
}
}
@ -119,7 +138,8 @@ impl NesWasm {
cart
} else {
log("ERROR Failed to load. Invalid ROM. Loading nestest instead.");
Cartridge::blow_dust_vec(include_bytes!("../../test-roms/nestest/nestest.nes").to_vec()).unwrap()
Cartridge::blow_dust_vec(include_bytes!("../../test-roms/nestest/nestest.nes").to_vec())
.unwrap()
};
log(format!("nes init! {}", cart).as_str());
@ -130,4 +150,4 @@ impl NesWasm {
pub fn tick(&mut self) {
self.nes.tick();
}
}
}

View file

@ -1,7 +1,8 @@
use core::{fmt::Display, ops::Range};
use alloc::vec::Vec;
use alloc::boxed::Box;
use alloc::vec::Vec;
use core::fmt::Display;
use core::ops::Range;
use common::kilobytes;
use self::error::CartridgeError;
@ -49,7 +50,7 @@ pub struct Header {
flags8: u8,
flags9: u8,
flags10: u8,
padding: [u8; 5]
padding: [u8; 5],
}
impl Header {
@ -64,7 +65,9 @@ impl Header {
}
Ok(Header {
magic: magic.try_into().map_err(|_| CartridgeError::InvalidCartridge("magic 2"))?,
magic: magic
.try_into()
.map_err(|_| CartridgeError::InvalidCartridge("magic 2"))?,
prg_rom_blocks: bin[4],
chr_rom_blocks: bin[5],
flags6: bin[6],
@ -72,17 +75,23 @@ impl Header {
flags8: bin[8],
flags9: bin[9],
flags10: bin[10],
padding: bin[11..16].try_into().map_err(|_| CartridgeError::InvalidCartridge("padding"))?,
padding: bin[11..16]
.try_into()
.map_err(|_| CartridgeError::InvalidCartridge("padding"))?,
})
}
pub fn total_size_excluding_header(&self) -> usize {
(self.prg_rom_blocks as usize * PRG_ROM_BLOCK_SIZE) + (self.chr_rom_blocks as usize * CHR_ROM_BLOCK_SIZE)
(self.prg_rom_blocks as usize * PRG_ROM_BLOCK_SIZE)
+ (self.chr_rom_blocks as usize * CHR_ROM_BLOCK_SIZE)
}
}
#[derive(Debug, PartialEq, Eq)]
enum Format { Nes2, Ines }
enum Format {
Nes2,
Ines,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum MapperType {
@ -104,7 +113,7 @@ impl TryFrom<&Header> for MapperType {
2 => Ok(MapperType::Uxrom),
3 => Ok(MapperType::Cnrom),
4 => Ok(MapperType::Mmc3),
_ => Err(CartridgeError::NotYetImplemented(format!("Mapper {}", id)))
_ => Err(CartridgeError::NotYetImplemented(format!("Mapper {}", id))),
}
}
}
@ -148,7 +157,7 @@ impl Rom for EmbeddedRom {
}
#[derive(PartialEq, Eq)]
pub struct Cartridge<R : Rom> {
pub struct Cartridge<R: Rom> {
rom: R,
mirroring: Mirroring,
prg: Range<usize>,
@ -179,23 +188,27 @@ impl Cartridge<EmbeddedRom> {
}
}
impl<R : Rom> Cartridge<R> {
impl<R: Rom> Cartridge<R> {
pub fn load(rom: R) -> Result<Cartridge<R>, CartridgeError> {
let bin = rom.get();
if bin.len() < HEADER_SIZE + PRG_ROM_BLOCK_SIZE || bin[0..4] != MAGIC {
return Err(CartridgeError::InvalidCartridge("strange size"));
}
let header = Header::parse(bin)?;
if header.magic != MAGIC {
return Err(CartridgeError::InvalidCartridge("magic"));
}
let format = if (header.flags7 & 0x0c) == 0x08 { Format::Nes2 } else { Format::Ines };
let format = if (header.flags7 & 0x0c) == 0x08 {
Format::Nes2
} else {
Format::Ines
};
// if format == Format::Nes2 {
// return Err(CartridgeError::NotYetImplemented("NES 2.0".into()));
// return Err(CartridgeError::NotYetImplemented("NES 2.0".into()));
// }
let mapper = MapperType::try_from(&header)?;
let skip_trainer = header.flags6 & 0b100 != 0;
@ -208,19 +221,21 @@ impl<R : Rom> Cartridge<R> {
// }
if header.flags6 & 0b1000 != 0 {
return Err(CartridgeError::NotYetImplemented("cartidge fiddles w VRAM address space..".into()));
return Err(CartridgeError::NotYetImplemented(
"cartidge fiddles w VRAM address space..".into(),
));
}
let mut mirroring = match header.flags6 & 1 {
1 => Mirroring::Vertical,
_ => Mirroring::Horizontal
_ => Mirroring::Horizontal,
};
if header.flags6 & 0b1000 != 0 {
mirroring = Mirroring::HardwiredFourScreen
}
let prg_size = (header.prg_rom_blocks as usize) * PRG_ROM_BLOCK_SIZE;
let prg_size = (header.prg_rom_blocks as usize) * PRG_ROM_BLOCK_SIZE;
let prg_start = HEADER_SIZE;
let prg_end = prg_start + prg_size;
@ -234,9 +249,8 @@ impl<R : Rom> Cartridge<R> {
chr_start..chr_end
};
let chr_ram = uses_chr_ram
.then_some(Box::new([0; CHR_ROM_BLOCK_SIZE]));
let chr_ram = uses_chr_ram.then_some(Box::new([0; CHR_ROM_BLOCK_SIZE]));
Ok(Cartridge {
prg: prg_start..prg_end,
chr: chr_range,
@ -245,7 +259,7 @@ impl<R : Rom> Cartridge<R> {
mapper,
format,
chr_ram,
prg_ram: Box::new([0; kilobytes::KB8])
prg_ram: Box::new([0; kilobytes::KB8]),
})
}
@ -260,7 +274,7 @@ impl<R : Rom> Cartridge<R> {
pub fn chr(&self) -> &[u8] {
// TODO: Perf, get rid of this branch
if let Some(chr_ram) = &self.chr_ram {
if let Some(chr_ram) = &self.chr_ram {
&chr_ram[..]
} else {
&self.rom.get()[self.chr.start..self.chr.end]
@ -284,19 +298,25 @@ impl<R : Rom> Cartridge<R> {
}
}
impl<R : Rom> Display for Cartridge<R> {
impl<R: Rom> Display for Cartridge<R> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let chr_ram_or_rom = if self.chr_ram.is_some() { " RAM" } else { "" };
write!(f,
"[{:?}] Mapper: {:?}, Mirroring: {:?}, CHR{}: {}x{}K, PRG: {}x{}K",
self.format, self.mapper, self.mirroring, chr_ram_or_rom,
self.chr().len() / CHR_ROM_BLOCK_SIZE, CHR_ROM_BLOCK_SIZE / 1000,
self.prg().len() / PRG_ROM_BLOCK_SIZE, PRG_ROM_BLOCK_SIZE / 1000
write!(
f,
"[{:?}] Mapper: {:?}, Mirroring: {:?}, CHR{}: {}x{}K, PRG: {}x{}K",
self.format,
self.mapper,
self.mirroring,
chr_ram_or_rom,
self.chr().len() / CHR_ROM_BLOCK_SIZE,
CHR_ROM_BLOCK_SIZE / 1000,
self.prg().len() / PRG_ROM_BLOCK_SIZE,
PRG_ROM_BLOCK_SIZE / 1000
)
}
}
impl<R : Rom> core::fmt::Debug for Cartridge<R> {
impl<R: Rom> core::fmt::Debug for Cartridge<R> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{self}")
}
@ -305,9 +325,9 @@ impl<R : Rom> core::fmt::Debug for Cartridge<R> {
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use crate::cartridge::EmbeddedRom;
use super::Cartridge;
use crate::cartridge::EmbeddedRom;
fn assert_cart(r: &'static [u8], s: &str) {
assert_eq!(Cartridge::load(EmbeddedRom(r)).unwrap().to_string(), s);
@ -321,16 +341,16 @@ mod tests {
#[test]
fn cart_valid_nrom() {
assert_cart(
include_bytes!("../../test-roms/nestest/nestest.nes"),
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K"
) ;
include_bytes!("../../test-roms/nestest/nestest.nes"),
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR: 1x8K, PRG: 1x16K",
);
}
#[test]
fn cart_valid_mmc1() {
assert_cart(
include_bytes!("../../test-roms/nes-test-roms/instr_test-v5/official_only.nes"),
"[Ines] Mapper: Mmc1, Mirroring: Vertical, CHR RAM: 1x8K, PRG: 16x16K"
"[Ines] Mapper: Mmc1, Mirroring: Vertical, CHR RAM: 1x8K, PRG: 16x16K",
);
}
@ -338,7 +358,7 @@ mod tests {
fn cart_valid_nrom_chr_ram() {
assert_cart(
include_bytes!("../../test-roms/nes-test-roms/blargg_ppu_tests_2005.09.15b/vram_access.nes"),
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR RAM: 1x8K, PRG: 1x16K"
"[Ines] Mapper: Nrom, Mirroring: Horizontal, CHR RAM: 1x8K, PRG: 1x16K",
);
}
}
}

View file

@ -1,10 +1,11 @@
use phf::phf_map;
use crate::frame::RenderFrame;
const W: usize = 5;
static FONTS: phf::Map<char, &'static str> = phf_map! {
'0' => r#"
'0' => r#"
....
. .
. .
@ -13,7 +14,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
. .
...."#,
'1' => r#"
'1' => r#"
..
.
.
@ -22,7 +23,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
."#,
'2' => r#"
'2' => r#"
...
.
.
@ -31,7 +32,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
...."#,
'3' => r#"
'3' => r#"
...
.
.
@ -40,7 +41,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
..."#,
'4' => r#"
'4' => r#"
. .
. .
. .
@ -49,7 +50,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
."#,
'5' => r#"
'5' => r#"
....
.
.
@ -58,7 +59,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
...."#,
'6' => r#"
'6' => r#"
....
.
.
@ -67,7 +68,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
. .
...."#,
'7' => r#"
'7' => r#"
....
.
.
@ -76,7 +77,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
. "#,
'8' => r#"
'8' => r#"
....
. .
. .
@ -85,7 +86,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
. .
...."#,
'9' => r#"
'9' => r#"
....
. .
. .
@ -93,7 +94,7 @@ static FONTS: phf::Map<char, &'static str> = phf_map! {
.
.
."#
};
};
pub fn draw(s: &str, pos: (usize, usize), frame: &mut RenderFrame) {
let fonts = s.chars().map(|c| FONTS.get(&c).expect("font missing"));
@ -103,19 +104,17 @@ pub fn draw(s: &str, pos: (usize, usize), frame: &mut RenderFrame) {
let mut x = char_base_x;
let mut y = pos.1;
font.chars().for_each(|c| {
match c {
'\n' => {
y += 1;
x = char_base_x;
},
'.' => {
frame.set_pixel_xy(x, y, (0xff, 0, 0));
x += 1;
}
' ' => x += 1,
_ => ()
font.chars().for_each(|c| match c {
'\n' => {
y += 1;
x = char_base_x;
}
'.' => {
frame.set_pixel_xy(x, y, (0xff, 0, 0));
x += 1;
}
' ' => x += 1,
_ => (),
});
}
}
}

View file

@ -62,8 +62,8 @@ pub struct RenderFrame {
impl RenderFrame {
pub fn new<FORMAT>() -> Self
where
FORMAT : PixelFormat + SetPixel + 'static
where
FORMAT: PixelFormat + SetPixel + 'static,
{
Self {
bytes_per_pixel: FORMAT::BYTES_PER_PIXEL,
@ -75,7 +75,7 @@ impl RenderFrame {
}
// The NES PPU always generates a 256x240 pixel picture.
pub fn set_pixel_xy(&mut self, x: usize, y: usize, rgb: (u8, u8, u8)) {
pub fn set_pixel_xy(&mut self, x: usize, y: usize, rgb: (u8, u8, u8)) {
let i = ((y * NES_WIDTH) + x) * self.bytes_per_pixel;
(self.set_pixel_fn)(&mut self.buf, i, rgb);
}
@ -92,7 +92,9 @@ impl RenderFrame {
pub fn pixels_ntsc(&self) -> impl Iterator<Item = u8> + '_ {
let buf_pitch = NES_WIDTH * self.bytes_per_pixel;
let overscan_pitch = NTSC_OVERSCAN_PIXELS * self.bytes_per_pixel;
self.buf.chunks(buf_pitch) // Chunk as rows
self
.buf
.chunks(buf_pitch) // Chunk as rows
.skip(NTSC_OVERSCAN_PIXELS) // Skip first X rows
.map(move |row| &row[overscan_pitch..buf_pitch - overscan_pitch]) // Skip col edges
.take(NTSC_HEIGHT)
@ -108,4 +110,3 @@ impl RenderFrame {
self.pitch_pal
}
}

View file

@ -17,18 +17,18 @@ bitflags! {
#[derive(Debug)]
pub enum JoypadEvent {
Press(JoypadButton),
Release(JoypadButton)
Release(JoypadButton),
}
#[derive(Default)]
pub struct Joypad {
state: JoypadButton,
out: u8
out: u8,
}
/*
bit 7 6 5 4 3 2 1 0
button A B Select Start Up Down Left Right
bit 7 6 5 4 3 2 1 0
button A B Select Start Up Down Left Right
*/
impl Joypad {
pub fn new() -> Self {
@ -43,7 +43,8 @@ impl Joypad {
}
pub fn strobe(&mut self, val: u8) {
if val & 1 == 1 { // Strobe is high
if val & 1 == 1 {
// Strobe is high
self.out = self.state.bits;
}
}
@ -54,4 +55,4 @@ impl Joypad {
JoypadEvent::Release(b) => self.state.set(b, false),
}
}
}
}

View file

@ -6,19 +6,22 @@ extern crate alloc;
pub use mos6502;
mod fonts;
mod mappers;
mod nesbus;
mod ppu;
mod fonts;
pub mod frame;
pub mod cartridge;
pub mod nes;
pub mod frame;
pub mod joypad;
pub mod nes;
pub mod trace {
#[derive(Debug)]
pub enum Tag { PpuTiming, Cpu }
pub enum Tag {
PpuTiming,
Cpu,
}
impl core::fmt::Display for Tag {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
@ -38,4 +41,4 @@ pub mod trace {
}
}};
}
}
}

View file

@ -1,27 +1,27 @@
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::{Cartridge, Rom};
use super::Mapper;
use crate::cartridge::Cartridge;
use crate::cartridge::Rom;
const BANK_SIZE: usize = kilobytes::KB8;
// Mapper 3
pub(crate) struct CNROM<R : Rom> {
pub(crate) struct CNROM<R: Rom> {
cart: Cartridge<R>,
selected_bank: usize,
is_16kb: bool,
}
impl<R : Rom> Mapper for CNROM<R> {}
impl<R: Rom> Mapper for CNROM<R> {}
impl<R: Rom> CNROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let is_16kb = match cart.prg().len() {
kilobytes::KB16 => true,
kilobytes::KB32 => false,
_ => panic!("invalid size for mapper 3 prg rom")
_ => panic!("invalid size for mapper 3 prg rom"),
};
Self {
@ -32,21 +32,18 @@ impl<R: Rom> CNROM<R> {
}
}
impl<R : Rom> Bus for CNROM<R> {
impl<R: Rom> Bus for CNROM<R> {
fn read8(&self, address: u16) -> u8 {
match address {
0x0000..=0x1fff => self.cart.chr()[(self.selected_bank * BANK_SIZE) + address as usize],
0x8000..=0xffff => {
if self.is_16kb {
self.cart.prg()[address as usize - 0x8000 - kilobytes::KB16] // see tests
}
else {
} else {
self.cart.prg()[address as usize - 0x8000]
}
}
_ => {
self.cart.prg_ram()[address as usize]
}
_ => self.cart.prg_ram()[address as usize],
}
}
@ -56,8 +53,8 @@ impl<R : Rom> Bus for CNROM<R> {
0x8000..=0xffff => {
self.selected_bank = (val & 0b00000011) as usize;
// println!("mapper 3 selected bank: {}", self.selected_bank);
},
_ => self.cart.prg_ram_mut()[address as usize] = val
}
_ => self.cart.prg_ram_mut()[address as usize] = val,
}
}
}
}

View file

@ -1,10 +1,12 @@
use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::{Cartridge, Mirroring, Rom};
use super::Mapper;
use crate::cartridge::Cartridge;
use crate::cartridge::Mirroring;
use crate::cartridge::Rom;
#[derive(Debug, PartialEq, Eq)]
enum PrgBankMode {
@ -19,7 +21,7 @@ impl From<u8> for PrgBankMode {
0 | 1 => PrgBankMode::Switch32Kb,
2 => PrgBankMode::FixFirstLowerSwitchUpper,
3 => PrgBankMode::FixLastUpperSwitchLower,
_ => panic!()
_ => panic!(),
}
}
}
@ -27,12 +29,12 @@ impl From<u8> for PrgBankMode {
#[derive(Debug, PartialEq, Eq)]
enum ChrBankMode {
Switch8Kb,
SwitchTwo4KbBanks
SwitchTwo4KbBanks,
}
pub struct MMC1<R : Rom> {
pub struct MMC1<R: Rom> {
cart: Cartridge<R>,
prg_rom_bank_mode: PrgBankMode,
prg_rom_bank_num: usize,
selected_prg_bank: u8,
@ -46,9 +48,9 @@ pub struct MMC1<R : Rom> {
shift_register: u8,
}
impl<R : Rom> Mapper for MMC1<R> {}
impl<R: Rom> Mapper for MMC1<R> {}
impl<R : Rom> MMC1<R> {
impl<R: Rom> MMC1<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let mirroring = cart.mirroring();
MMC1 {
@ -63,8 +65,8 @@ impl<R : Rom> MMC1<R> {
shift_register: 0,
num_shift_writes: 0,
selected_prg_bank: 0,
mirroring
}
mirroring,
}
}
fn reset_shift_register(&mut self) {
@ -79,7 +81,7 @@ impl<R : Rom> MMC1<R> {
}
let bit_to_write = val & 1;
// shift in bit, lsb first. max width of shift reg is 5 bits, so we only shift to bit 4.
self.shift_register = (self.shift_register >> 1) | (bit_to_write << 4);
@ -87,19 +89,23 @@ impl<R : Rom> MMC1<R> {
if self.num_shift_writes == 5 {
match address {
0x8000..=0x9fff => { // Control
0x8000..=0x9fff => {
// Control
self.update_control_register(self.shift_register);
}
0xa000..=0xbfff => { // CHR bank 0
0xa000..=0xbfff => {
// CHR bank 0
self.switch_lower_chr_bank(self.shift_register)
}
0xc000..=0xdfff => { // CHR bank 1
0xc000..=0xdfff => {
// CHR bank 1
self.switch_upper_chr_bank(self.shift_register)
}
0xe000..=0xffff => { // PRG bank
0xe000..=0xffff => {
// PRG bank
self.selected_prg_bank = self.shift_register & 0b01111;
}
_ => panic!("unknown register")
_ => panic!("unknown register"),
}
self.reset_shift_register()
@ -128,13 +134,13 @@ impl<R : Rom> MMC1<R> {
1 => Mirroring::SingleScreenUpper,
2 => Mirroring::Vertical,
3 => Mirroring::Horizontal,
_ => unreachable!()
_ => unreachable!(),
};
let chr_rom_bank_mode = (val & 0b10000) >> 4;
self.chr_rom_bank_mode = match chr_rom_bank_mode {
0 => ChrBankMode::Switch8Kb,
_ => ChrBankMode::SwitchTwo4KbBanks
_ => ChrBankMode::SwitchTwo4KbBanks,
};
let prg_rom_bank_mode = (val & 0b01100) >> 2;
@ -180,7 +186,7 @@ impl<R : Rom> MMC1<R> {
}
}
impl <R : Rom>Bus for MMC1<R> {
impl<R: Rom> Bus for MMC1<R> {
fn read8(&self, address: u16) -> u8 {
// println!("Read: {:#06x}", address);
match address {
@ -193,7 +199,7 @@ impl <R : Rom>Bus for MMC1<R> {
0x8000..=0xbfff => self.lower_prg_bank()[address as usize - 0x8000],
0xc000..=0xffff => self.upper_prg_bank()[address as usize - 0xc000],
// TODO: In most mappers, banks past the end of PRG or CHR ROM show up as mirrors of earlier banks.
_ => 0//panic!("unknown mmc1 memory range")
_ => 0, //panic!("unknown mmc1 memory range")
}
}
@ -206,7 +212,7 @@ impl <R : Rom>Bus for MMC1<R> {
// CPU
0x6000..=0x7fff => self.cart.prg_ram_mut()[address as usize - 0x6000] = val,
0x8000..=0xffff => self.write_to_shift_register(val, address),
_ => () //panic!("writing to rom: {:#06x}", address)
_ => (), //panic!("writing to rom: {:#06x}", address)
}
}
}
}

View file

@ -1,10 +1,13 @@
use alloc::boxed::Box;
use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use alloc::boxed::Box;
use crate::cartridge::{Cartridge, Mirroring, Rom};
use super::Mapper;
use crate::cartridge::Cartridge;
use crate::cartridge::Mirroring;
use crate::cartridge::Rom;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum PrgBankMode {
@ -18,9 +21,9 @@ enum ChrBankMode {
TwoKbAt1000_1 = 1,
}
pub struct MMC3<R : Rom> {
pub struct MMC3<R: Rom> {
cart: Cartridge<R>,
prg_rom_banks_total: usize,
prg_rom_bank_mode: PrgBankMode,
@ -29,14 +32,14 @@ pub struct MMC3<R : Rom> {
registers: [u8; 8],
register_to_update: u8, // 3 bits
irq_enabled: bool,
irq_latch: u8,
irq_counter: u8,
irq_reload: bool,
}
impl<R : Rom> MMC3<R> {
impl<R: Rom> MMC3<R> {
pub fn new(cart: Cartridge<R>) -> Self {
Self {
prg_rom_banks_total: cart.prg().len() / kilobytes::KB8,
@ -50,7 +53,7 @@ impl<R : Rom> MMC3<R> {
irq_latch: 0,
irq_counter: 0,
irq_reload: false,
}
}
}
// https://www.nesdev.org/wiki/MMC3#PRG_Banks
@ -64,7 +67,7 @@ impl<R : Rom> MMC3<R> {
(0xc000..=0xdfff, 0) => second_last_bank,
(0xc000..=0xdfff, 1) => self.registers[6] as usize,
(0xe000..=0xffff, _) => self.prg_rom_banks_total - 1,
_ => panic!()
_ => panic!(),
};
// Remove top bank indexing bits - 0x1fff == 8kb - 1
@ -76,8 +79,8 @@ impl<R : Rom> MMC3<R> {
fn read_chr(&self, address: u16) -> u8 {
let d7 = self.chr_rom_bank_mode as u8;
// R0 and R1 ignore the bottom bit, as the value written still
// counts banks in 1KB units but odd numbered banks can't be selected.
// R0 and R1 ignore the bottom bit, as the value written still
// counts banks in 1KB units but odd numbered banks can't be selected.
let bank: u8 = match (address, d7) {
(0x0000..=0x03FF, 0) => self.registers[0],
(0x0000..=0x03FF, 1) => self.registers[2],
@ -95,17 +98,17 @@ impl<R : Rom> MMC3<R> {
(0x1800..=0x1BFF, 1) => self.registers[1],
(0x1C00..=0x1FFF, 0) => self.registers[5],
(0x1C00..=0x1FFF, 1) => self.registers[1] | 1,
_ => panic!()
_ => panic!(),
};
// Remove top bank indexing bits - 0x03ff == 1kb - 1
let offset = address as usize & 0x3ff;
let offset = address as usize & 0x3ff;
let base = kilobytes::KB1 * bank as usize;
self.cart.chr()[base + offset]
}
}
impl<R : Rom> Mapper for MMC3<R> {
impl<R: Rom> Mapper for MMC3<R> {
fn on_runtime_mirroring(&mut self, cb: Box<dyn FnMut(&Mirroring)>) {
self.mirroring_cb = Some(cb);
}
@ -127,15 +130,14 @@ impl<R : Rom> Mapper for MMC3<R> {
}
}
impl<R : Rom> Bus for MMC3<R> {
impl<R: Rom> Bus for MMC3<R> {
fn read8(&self, address: u16) -> u8 {
// println!("Read: {:#06x}", address);
match address {
0x0000..=0x1fff => self.read_chr(address),
0x6000..=0x7fff => self.cart.prg_ram()[address as usize - 0x6000],
0x8000..=0xffff => self.read_prg(address),
_ => 0
_ => 0,
}
}
@ -150,8 +152,16 @@ impl<R : Rom> Bus for MMC3<R> {
0x8000..=0x9fff => {
if even {
// Bank select
self.prg_rom_bank_mode = if val & 0x40 == 0 { PrgBankMode::Swap8000FixC000_0 } else { PrgBankMode::SwapC000Fix8000_1 };
self.chr_rom_bank_mode = if val & 0x80 == 0 { ChrBankMode::TwoKbAt0000_0 } else { ChrBankMode::TwoKbAt1000_1 };
self.prg_rom_bank_mode = if val & 0x40 == 0 {
PrgBankMode::Swap8000FixC000_0
} else {
PrgBankMode::SwapC000Fix8000_1
};
self.chr_rom_bank_mode = if val & 0x80 == 0 {
ChrBankMode::TwoKbAt0000_0
} else {
ChrBankMode::TwoKbAt1000_1
};
self.register_to_update = val & 0b111;
} else {
// Bank data
@ -160,20 +170,21 @@ impl<R : Rom> Bus for MMC3<R> {
self.registers[self.register_to_update as usize] = match self.register_to_update {
0 | 1 => val & 0xfe,
6 | 7 => val & 0x3f,
_ => val
_ => val,
};
}
}
0xa000..=0xbfff => {
if even {
let runtime_mirroring = if val & 1 == 1 {
Mirroring::Horizontal
} else {
Mirroring::Vertical
let runtime_mirroring = if val & 1 == 1 {
Mirroring::Horizontal
} else {
Mirroring::Vertical
};
if self.cart.mirroring() != runtime_mirroring {
let cb = self.mirroring_cb
let cb = self
.mirroring_cb
.as_mut()
.expect("mirroring changed, no one to tell");
(*cb)(&runtime_mirroring)
@ -196,7 +207,7 @@ impl<R : Rom> Bus for MMC3<R> {
}
self.irq_enabled = !even;
}
_ => ()
_ => (),
}
}
}
}

View file

@ -1,26 +1,32 @@
use core::cell::RefCell;
use alloc::rc::Rc;
use mos6502::memory::Bus;
use alloc::boxed::Box;
use crate::cartridge::{Cartridge, Mirroring, Rom};
use alloc::rc::Rc;
use core::cell::RefCell;
use mos6502::memory::Bus;
use crate::cartridge::Cartridge;
use crate::cartridge::Mirroring;
use crate::cartridge::Rom;
mod mmc1;
mod nrom;
mod cnrom;
mod mmc1;
mod mmc3;
mod nrom;
mod uxrom;
pub trait Mapper : Bus {
pub trait Mapper: Bus {
fn on_runtime_mirroring(&mut self, _: Box<dyn FnMut(&Mirroring)>) {}
fn irq(&mut self) -> bool { false }
fn irq(&mut self) -> bool {
false
}
}
pub(crate) fn for_cart<R : Rom + 'static>(cart: Cartridge<R>) -> Rc<RefCell<dyn Mapper>> {
pub(crate) fn for_cart<R: Rom + 'static>(cart: Cartridge<R>) -> Rc<RefCell<dyn Mapper>> {
match cart.mapper_type() {
crate::cartridge::MapperType::Nrom => Rc::new(RefCell::new(nrom::NROM::new(cart))),
crate::cartridge::MapperType::Mmc1 => Rc::new(RefCell::new(mmc1::MMC1::new(cart))),
crate::cartridge::MapperType::Uxrom => Rc::new(RefCell::new(uxrom::UxROM::new(cart))),
crate::cartridge::MapperType::Cnrom => Rc::new(RefCell::new(cnrom::CNROM::new(cart))),
crate::cartridge::MapperType::Mmc3 => Rc::new(RefCell::new(mmc3::MMC3::new(cart)))
crate::cartridge::MapperType::Mmc3 => Rc::new(RefCell::new(mmc3::MMC3::new(cart))),
}
}
}

View file

@ -3,32 +3,29 @@ use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::{Cartridge, Rom};
use super::Mapper;
use crate::cartridge::Cartridge;
use crate::cartridge::Rom;
pub struct NROM<R : Rom> {
pub struct NROM<R: Rom> {
cart: Cartridge<R>,
is_16kb: bool
is_16kb: bool,
}
impl<R : Rom> Mapper for NROM<R> {}
impl<R: Rom> Mapper for NROM<R> {}
impl<R : Rom> NROM<R> {
impl<R: Rom> NROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let is_16kb = match cart.prg().len() {
kilobytes::KB16 => true,
kilobytes::KB32 => false,
_ => panic!("invalid size for NROM prg rom")
_ => panic!("invalid size for NROM prg rom"),
};
Self {
cart,
is_16kb
}
Self { cart, is_16kb }
}
}
impl<R : Rom> Bus for NROM<R> {
impl<R: Rom> Bus for NROM<R> {
fn read8(&self, address: u16) -> u8 {
match address {
0x0000..=0x1fff => self.cart.chr()[address as usize], // PPU
@ -39,13 +36,12 @@ impl<R : Rom> Bus for NROM<R> {
if self.is_16kb {
// Mirror
self.cart.prg()[address as usize - 0xc000]
}
else {
} else {
// last 16kb of rom
self.cart.prg()[kilobytes::KB16 + (address as usize - 0xc000)]
}
}
_ => 0//panic!("unknown NROM memory range: {:#06x}", address)
_ => 0, //panic!("unknown NROM memory range: {:#06x}", address)
}
}
@ -55,4 +51,4 @@ impl<R : Rom> Bus for NROM<R> {
_ => (),
}
}
}
}

View file

@ -1,29 +1,29 @@
use common::kilobytes;
use mos6502::memory::Bus;
use crate::cartridge::{Cartridge, Rom};
use super::Mapper;
use crate::cartridge::Cartridge;
use crate::cartridge::Rom;
pub struct UxROM<R : Rom> {
pub struct UxROM<R: Rom> {
cart: Cartridge<R>,
bank: u8,
num_banks: usize,
}
impl<R : Rom> Mapper for UxROM<R> {}
impl<R: Rom> Mapper for UxROM<R> {}
impl<R : Rom> UxROM<R> {
impl<R: Rom> UxROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
Self {
Self {
num_banks: cart.prg().len() / kilobytes::KB16,
cart,
bank: 0
cart,
bank: 0,
}
}
}
impl<R : Rom> Bus for UxROM<R> {
impl<R: Rom> Bus for UxROM<R> {
fn read8(&self, address: u16) -> u8 {
let address = address as usize;
let selected_bank = self.bank as usize;
@ -32,7 +32,7 @@ impl<R : Rom> Bus for UxROM<R> {
0x0000..=0x1fff => self.cart.chr()[address],
0x8000..=0xbfff => self.cart.prg()[(selected_bank * kilobytes::KB16) + (address - 0x8000)],
0xc000..=0xffff => self.cart.prg()[(last_bank * kilobytes::KB16) + (address - 0xc000)],
_ => 0
_ => 0,
}
}
@ -40,7 +40,7 @@ impl<R : Rom> Bus for UxROM<R> {
match address {
0x0000..=0x1fff => self.cart.chr_ram()[address as usize] = val,
0x8000..=0xffff => self.bank = val,
_ => ()
_ => (),
}
}
}
}

View file

@ -1,22 +1,57 @@
use core::{cell::RefCell, time::Duration};
use alloc::{rc::Rc, boxed::Box, string::ToString};
use alloc::boxed::Box;
use alloc::rc::Rc;
use alloc::string::ToString;
use core::cell::RefCell;
use core::time::Duration;
#[allow(unused_imports)]
use mos6502::{mos6502::Mos6502, memory::Bus, cpu::{Cpu, Reg}, debugger::Debugger};
use crate::{cartridge::{Cartridge, Rom}, nesbus::NesBus, ppu::ppu::{Ppu, TickEvent}, joypad::Joypad, frame::{RenderFrame, PixelFormatRGB888, PixelFormatRGB565}, trace, fonts};
use mos6502::cpu::Cpu;
#[allow(unused_imports)]
use mos6502::cpu::Reg;
#[allow(unused_imports)]
use mos6502::debugger::Debugger;
#[allow(unused_imports)]
use mos6502::memory::Bus;
#[allow(unused_imports)]
use mos6502::mos6502::Mos6502;
use crate::cartridge::Cartridge;
use crate::cartridge::Rom;
use crate::fonts;
use crate::frame::PixelFormatRGB565;
use crate::frame::PixelFormatRGB888;
use crate::frame::RenderFrame;
use crate::joypad::Joypad;
use crate::nesbus::NesBus;
use crate::ppu::ppu::Ppu;
use crate::ppu::ppu::TickEvent;
use crate::trace;
const DEFAULT_FPS_MAX: usize = 60;
#[derive(PartialEq, Eq)]
pub enum Shutdown { Yes, No, Reset }
pub enum Shutdown {
Yes,
No,
Reset,
}
impl From<bool> for Shutdown {
fn from(b: bool) -> Self {
if b { Shutdown::Yes } else { Shutdown::No }
if b {
Shutdown::Yes
} else {
Shutdown::No
}
}
}
#[derive(PartialEq, Default)]
pub enum HostPixelFormat { #[default] Rgb888, Rgb565 }
pub enum HostPixelFormat {
#[default]
Rgb888,
Rgb565,
}
pub trait HostPlatform {
fn render(&mut self, frame: &RenderFrame);
@ -31,12 +66,14 @@ pub trait HostPlatform {
// Not required. Up to platform to implement for FPS control.
}
fn pixel_format(&self) -> HostPixelFormat { HostPixelFormat::default() }
fn pixel_format(&self) -> HostPixelFormat {
HostPixelFormat::default()
}
fn alloc_render_frame(&self) -> RenderFrame {
match self.pixel_format() {
HostPixelFormat::Rgb888 => RenderFrame::new::<PixelFormatRGB888>(),
HostPixelFormat::Rgb565 => RenderFrame::new::<PixelFormatRGB565>()
HostPixelFormat::Rgb565 => RenderFrame::new::<PixelFormatRGB565>(),
}
}
}
@ -45,8 +82,12 @@ pub trait HostPlatform {
struct HeadlessHost;
impl HostPlatform for HeadlessHost {
fn render(&mut self, _: &RenderFrame) {}
fn poll_events(&mut self, _: &mut Joypad) -> Shutdown { Shutdown::No }
fn elapsed_millis(&self) -> usize { 0 }
fn poll_events(&mut self, _: &mut Joypad) -> Shutdown {
Shutdown::No
}
fn elapsed_millis(&self) -> usize {
0
}
fn delay(&self, _: Duration) {}
}
@ -57,11 +98,14 @@ pub struct Nes {
joypad: Rc<RefCell<Joypad>>,
timing: FrameTiming,
show_fps: bool,
shutdown: Shutdown
shutdown: Shutdown,
}
impl Nes {
pub fn insert<H : HostPlatform + 'static, R : Rom + 'static>(cartridge: Cartridge<R>, host: H) -> Self {
pub fn insert<H: HostPlatform + 'static, R: Rom + 'static>(
cartridge: Cartridge<R>,
host: H,
) -> Self {
let mirroring = cartridge.mirroring();
let rom_mapper = crate::mappers::for_cart(cartridge);
@ -76,18 +120,18 @@ impl Nes {
let mut machine = Mos6502::new(cpu);
machine.inc_cycles(7); // Startup cycles..
Self {
Self {
machine,
ppu,
host: Box::new(host),
joypad,
timing: FrameTiming::new(),
shutdown: Shutdown::No,
show_fps: false
show_fps: false,
}
}
pub fn insert_headless_host<R : Rom + 'static>(cartridge: Cartridge<R>) -> Self {
pub fn insert_headless_host<R: Rom + 'static>(cartridge: Cartridge<R>) -> Self {
Self::insert(cartridge, HeadlessHost::default())
}
@ -96,7 +140,7 @@ impl Nes {
let mut ppu = self.ppu.borrow_mut();
let ppu_event = ppu.tick(cpu_cycles * 3);
if ppu_event == TickEvent::EnteredVblank {
trace!(Tag::PpuTiming, "VBLANK");
@ -104,10 +148,10 @@ impl Nes {
let fps = self.timing.fps_avg(self.host.elapsed_millis());
fonts::draw(fps.to_string().as_str(), (10, 10), ppu.frame_mut());
}
self.host.render(ppu.frame());
self.shutdown = self.host.poll_events(&mut self.joypad.borrow_mut());
if let Some(delay)= self.timing.post_render(self.host.elapsed_millis()) {
if let Some(delay) = self.timing.post_render(self.host.elapsed_millis()) {
self.host.delay(delay);
}
self.timing.post_delay(self.host.elapsed_millis());
@ -148,7 +192,7 @@ impl Nes {
pub fn cpu_ticks(&self) -> usize {
self.machine.ticks()
}
pub fn fps_max(&mut self, fps_max: usize) {
self.timing.fps_max(fps_max);
}
@ -170,7 +214,11 @@ struct FrameTiming {
impl FrameTiming {
pub fn new() -> Self {
Self { frame_n: 0, last_frame_timestamp: 0, frame_limit_ms: 1000 / DEFAULT_FPS_MAX }
Self {
frame_n: 0,
last_frame_timestamp: 0,
frame_limit_ms: 1000 / DEFAULT_FPS_MAX,
}
}
pub fn fps_max(&mut self, fps_max: usize) {
@ -191,7 +239,9 @@ impl FrameTiming {
let ms_to_render_frame = elapsed - self.last_frame_timestamp;
// println!("took: {}ms, target: {}ms", ms_to_render_frame, self.frame_limit_ms);
if ms_to_render_frame < self.frame_limit_ms {
return Some(Duration::from_millis((self.frame_limit_ms - ms_to_render_frame) as u64));
return Some(Duration::from_millis(
(self.frame_limit_ms - ms_to_render_frame) as u64,
));
}
}
@ -213,22 +263,33 @@ impl core::fmt::Debug for Nes {
let ppu_cycle = self.ppu.borrow_mut().cycle() + 21;
let ppuw = 3;
if ppu_cycle < 100 {
write!(f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$}, {:>2} CYC:{}",
write!(
f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$}, {:>2} CYC:{}",
c.pc(),
c[Reg::AC], c[Reg::X], c[Reg::Y], c.flags_as_byte(), c[Reg::SP],
scanline, ppu_cycle,
c[Reg::AC],
c[Reg::X],
c[Reg::Y],
c.flags_as_byte(),
c[Reg::SP],
scanline,
ppu_cycle,
self.machine.cycles()
)
}
else {
write!(f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$},{:>2} CYC:{}",
} else {
write!(
f,
"{:04X} A:{:02X} X:{:02X} Y:{:02X} P:{:02X} SP:{:02X} PPU:{:ppuw$},{:>2} CYC:{}",
c.pc(),
c[Reg::AC], c[Reg::X], c[Reg::Y], c.flags_as_byte(), c[Reg::SP],
scanline, ppu_cycle,
c[Reg::AC],
c[Reg::X],
c[Reg::Y],
c.flags_as_byte(),
c[Reg::SP],
scanline,
ppu_cycle,
self.machine.cycles()
)
}
}
}
}

View file

@ -1,16 +1,18 @@
use alloc::rc::Rc;
use core::cell::RefCell;
use alloc::{rc::Rc};
use common::kilobytes;
use mos6502::memory::Bus;
use crate::{ppu::ppu::Ppu, joypad::Joypad, mappers::Mapper};
use crate::joypad::Joypad;
use crate::mappers::Mapper;
use crate::ppu::ppu::Ppu;
pub struct NesBus {
ram: [u8; kilobytes::KB2],
rom: Rc<RefCell<dyn Mapper>>,
ppu: Rc<RefCell<Ppu>>,
joypad: Rc<RefCell<Joypad>>
joypad: Rc<RefCell<Joypad>>,
}
#[derive(Debug, PartialEq, Eq)]
@ -25,12 +27,16 @@ enum MappedDevice {
}
impl NesBus {
pub fn new(rom: Rc<RefCell<dyn Mapper>>, ppu: Rc<RefCell<Ppu>>, joypad: Rc<RefCell<Joypad>>) -> Self {
Self {
pub fn new(
rom: Rc<RefCell<dyn Mapper>>,
ppu: Rc<RefCell<Ppu>>,
joypad: Rc<RefCell<Joypad>>,
) -> Self {
Self {
rom,
ram: [0; kilobytes::KB2],
ppu,
joypad
joypad,
}
}
@ -40,7 +46,7 @@ impl NesBus {
0x0800..=0x1fff => (MappedDevice::Ram, address & 0x07ff),
0x2000..=0x2007 => (MappedDevice::Ppu, address - 0x2000),
0x2008..=0x3fff => (MappedDevice::Ppu, address % 8),
0x4014 => (MappedDevice::PpuOamDma, address),
0x4014 => (MappedDevice::PpuOamDma, address),
0x4000..=0x4015 => (MappedDevice::Apu, address - 0x4000),
0x4016..=0x4017 => (MappedDevice::Joypad, address),
0x4018..=0x401f => (MappedDevice::CpuTest, address - 0x4018),
@ -60,8 +66,8 @@ impl Bus for NesBus {
MappedDevice::Joypad => {
match address {
0x4016 => self.joypad.borrow_mut().read(), // Joystick 1 data
0x4017 => 0, // Joystick 2 data
_ => unreachable!()
0x4017 => 0, // Joystick 2 data
_ => unreachable!(),
}
}
MappedDevice::CpuTest => 0,
@ -74,21 +80,24 @@ impl Bus for NesBus {
match device {
MappedDevice::Ram => self.ram[mapped_address as usize] = val,
MappedDevice::Ppu => self.ppu.borrow_mut().cpu_write_register(val, mapped_address),
MappedDevice::Ppu => self
.ppu
.borrow_mut()
.cpu_write_register(val, mapped_address),
MappedDevice::Apu => (),
MappedDevice::PpuOamDma => {
// Dump CPU page XX00..XXFF to PPU OAM
let page_start = (val as u16) << 8;
let mem = (page_start..=page_start+0xff).map(|addr| self.read8(addr));
let mem = (page_start..=page_start + 0xff).map(|addr| self.read8(addr));
// println!("{:#04x} - dumping {:#06x}..{:#06x}", val, page_start, page_start+0xff);
self.ppu.borrow_mut().cpu_oam_dma(mem);
}
MappedDevice::Joypad => {
match address {
0x4016 => self.joypad.borrow_mut().strobe(val), // Joystick strobe
0x4017 => (), // APU Frame counter control
_ => unreachable!()
}
0x4017 => (), // APU Frame counter control
_ => unreachable!(),
}
}
MappedDevice::CpuTest => (),
MappedDevice::Cartridge => self.rom.borrow_mut().write8(val, address),
@ -98,10 +107,12 @@ impl Bus for NesBus {
#[cfg(test)]
mod tests {
use crate::{frame::{RenderFrame, PixelFormatRGB888}, cartridge::Mirroring};
use super::*;
use crate::cartridge::Mirroring;
use crate::frame::PixelFormatRGB888;
use crate::frame::RenderFrame;
struct TestBus{}
struct TestBus {}
impl Mapper for TestBus {}
@ -116,20 +127,20 @@ mod tests {
}
fn sut() -> NesBus {
let bus = Rc::new(RefCell::new(TestBus{}));
let bus = Rc::new(RefCell::new(TestBus {}));
let joypad = Joypad::default();
let frame = RenderFrame::new::<PixelFormatRGB888>();
NesBus::new(
bus.clone(),
bus.clone(),
Rc::new(RefCell::new(Ppu::new(bus, Mirroring::Horizontal, frame))),
Rc::new(RefCell::new(joypad))
Rc::new(RefCell::new(joypad)),
)
}
#[test]
fn test_map_ram_mirror() {
let bus = sut();
assert_eq!(bus.map(0x07ff), (MappedDevice::Ram, 0x07ff));
assert_eq!(bus.map(0x0800), (MappedDevice::Ram, 0x0000));
assert_eq!(bus.map(0x1fff), (MappedDevice::Ram, 0x07ff));
@ -139,7 +150,7 @@ mod tests {
#[test]
fn test_map_ppu_mirror() {
let bus = sut();
assert_eq!(bus.map(0x2000), (MappedDevice::Ppu, 0));
assert_eq!(bus.map(0x3456), (MappedDevice::Ppu, 6));
assert_eq!(bus.map(0x2008), (MappedDevice::Ppu, 0));
@ -159,4 +170,4 @@ mod tests {
assert_eq!(bus.map(a), (MappedDevice::Ppu, 0));
}
}
}
}

View file

@ -1,5 +1,5 @@
mod palette;
mod vram;
mod state;
mod vram;
pub(crate) mod ppu;
pub(crate) mod ppu;

View file

@ -2,66 +2,60 @@ const PALETTE_SIZE: usize = 32;
// AKA boot palette?
pub const BLARRG_PALETTE: [u8; PALETTE_SIZE] = [
0x09,0x01,0x00,0x01,
0x00,0x02,0x02,0x0D,
0x08,0x10,0x08,0x24,
0x00,0x00,0x04,0x2C,
0x09,0x01,0x34,0x03,
0x00,0x04,0x00,0x14,
0x08,0x3A,0x00,0x02,
0x00,0x20,0x2C,0x08
0x09, 0x01, 0x00, 0x01, 0x00, 0x02, 0x02, 0x0D, 0x08, 0x10, 0x08, 0x24, 0x00, 0x00, 0x04, 0x2C,
0x09, 0x01, 0x34, 0x03, 0x00, 0x04, 0x00, 0x14, 0x08, 0x3A, 0x00, 0x02, 0x00, 0x20, 0x2C, 0x08,
];
// https://www.nesdev.org/wiki/PPU_palettes
static PALETTE_RGB: [(u8, u8, u8); 64] = [
(101, 101, 101),
(0 , 45, 105),
(19, 31, 127),
(69 , 19, 124),
(96 , 11, 98),
(115, 10, 55),
(113, 15, 7),
(90 , 26, 0),
(52 , 40, 0),
(11 , 52, 0),
(0, 60, 0),
(0, 61, 16),
(0, 56, 64),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(174,174 ,174),
(15 , 99,179),
(64 , 81, 208),
(120, 65, 204),
(167, 54, 169),
(192, 52, 112),
(189, 60, 48),
(159, 74, 0),
(109, 92, 0),
(54 , 109 , 0),
(7 , 119 , 4),
(0 , 121 , 61),
(0, 114 ,125),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 45, 105),
(19, 31, 127),
(69, 19, 124),
(96, 11, 98),
(115, 10, 55),
(113, 15, 7),
(90, 26, 0),
(52, 40, 0),
(11, 52, 0),
(0, 60, 0),
(0, 61, 16),
(0, 56, 64),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(174, 174, 174),
(15, 99, 179),
(64, 81, 208),
(120, 65, 204),
(167, 54, 169),
(192, 52, 112),
(189, 60, 48),
(159, 74, 0),
(109, 92, 0),
(54, 109, 0),
(7, 119, 4),
(0, 121, 61),
(0, 114, 125),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(254, 254, 255),
(93, 179, 255),
(93, 179, 255),
(143, 161, 255),
(200, 144, 255),
(247, 133, 250),
(255, 131, 192),
(255, 139, 127),
(239, 154, 73),
(189, 172, 44),
(133, 188, 47),
(85, 199, 83),
(60, 201, 140),
(62, 194, 205),
(78, 78, 78),
(0, 0, 0),
(0, 0, 0),
(239, 154, 73),
(189, 172, 44),
(133, 188, 47),
(85, 199, 83),
(60, 201, 140),
(62, 194, 205),
(78, 78, 78),
(0, 0, 0),
(0, 0, 0),
(254, 254, 255),
(188, 223, 255),
(209, 216, 255),
@ -72,22 +66,22 @@ static PALETTE_RGB: [(u8, u8, u8); 64] = [
(248, 213, 180),
(228, 220, 168),
(204, 227, 169),
(185, 232, 184),
(174, 232, 208),
(185, 232, 184),
(174, 232, 208),
(175, 229, 234),
(182, 182, 182),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
];
pub struct Palette {
data: [u8; PALETTE_SIZE]
data: [u8; PALETTE_SIZE],
}
impl Palette {
pub fn new() -> Self {
Self {
data: BLARRG_PALETTE
data: BLARRG_PALETTE,
}
}
@ -111,16 +105,16 @@ impl Palette {
let mirrored = match address {
0x3f00..=0x3f1f => address,
0x3f20..=0x3fff => 0x3f00 + (address % PALETTE_SIZE as u16),
_ => panic!("invalid palette address: {:#06x}", address)
_ => panic!("invalid palette address: {:#06x}", address),
};
// Special palette crap: Addresses $3F10/$3F14/$3F18/$3F1C are mirrors of $3F00/$3F04/$3F08/$3F0C
match mirrored {
0x3f10 => 0x3f00,
0x3f14 => 0x3f04,
0x3f18 => 0x3f08,
0x3f1c => 0x3f0c,
_ => mirrored
_ => mirrored,
}
}
}
@ -143,4 +137,4 @@ mod tests {
assert_eq!(Palette::mirror(0x3f18), 0x3f08);
assert_eq!(Palette::mirror(0x3f1c), 0x3f0c);
}
}
}

View file

@ -1,22 +1,29 @@
use core::cell::RefCell;
use alloc::rc::Rc;
use alloc::vec::Vec;
use crate::{frame::RenderFrame, trace, ppu::state::{Phase, Rendering}, mappers::Mapper, cartridge::Mirroring};
use super::{palette::Palette, vram::Vram, state::State};
use core::cell::RefCell;
use super::palette::Palette;
use super::state::State;
use super::vram::Vram;
use crate::cartridge::Mirroring;
use crate::frame::RenderFrame;
use crate::mappers::Mapper;
use crate::ppu::state::Phase;
use crate::ppu::state::Rendering;
use crate::trace;
#[derive(Default, Clone, Copy, Debug)]
struct Sprite {
pixels: [u8; 8], // Only 8 pixels per line
priority: bool, // Priority (0: in front of background; 1: behind background)
priority: bool, // Priority (0: in front of background; 1: behind background)
x: u8,
zero: bool
zero: bool,
}
#[derive(Debug)]
#[repr(u16)]
#[allow(dead_code)]
enum Register {
enum Register {
Ctrl2000 = 0, // ... + base 0x2000
Mask2001 = 1,
Status2002 = 2,
@ -25,7 +32,7 @@ enum Register {
Scroll2005 = 5,
Addr2006 = 6,
Data2007 = 7,
OamDma2008 = 8
OamDma2008 = 8,
}
impl From<u16> for Register {
@ -35,7 +42,11 @@ impl From<u16> for Register {
}
#[derive(PartialEq, Eq)]
pub enum TickEvent { Nothing, EnteredVblank, TriggerIrq }
pub enum TickEvent {
Nothing,
EnteredVblank,
TriggerIrq,
}
#[allow(dead_code)]
pub struct Ppu {
@ -49,9 +60,9 @@ pub struct Ppu {
oam_address: u8,
sprites: Vec<Sprite>, // AKA secondary OAM
v: u16, // Current VRAM address (15 bits)
v: u16, // Current VRAM address (15 bits)
t: u16, // Temporary VRAM address (15 bits); can also be thought of as the address of the top left onscreen tile.
fine_x: u8, // Fine X scroll (3 bits)
fine_x: u8, // Fine X scroll (3 bits)
w_latch: bool,
in_vblank: bool,
@ -116,7 +127,6 @@ impl Ppu {
}
}
pub fn cpu_read_register(&mut self, address: u16) -> u8 {
match Register::from(address) {
Register::Status2002 => {
@ -133,7 +143,7 @@ impl Ppu {
self.in_vblank = false;
self.w_latch = true;
status
},
}
Register::OamData2004 => self.oam[self.oam_address as usize],
Register::Data2007 => {
let address = self.v & 0x3fff; // 14 bits wide
@ -142,18 +152,18 @@ impl Ppu {
0x2000..=0x2fff => self.vram.read(address),
0x3000..=0x3eff => self.vram.read(address - 0x1000),
0x3f00..=0x3fff => self.palette.read(address),
_ => panic!("invalid read: {:#06x}", address)
_ => panic!("invalid read: {:#06x}", address),
};
let return_value = match address {
0..=0x3eff => self.data_buffer,
_ => value // Palette is not buffered
_ => value, // Palette is not buffered
};
self.data_buffer = value;
self.inc_v();
return_value
}
_ => 0
_ => 0,
}
}
@ -169,14 +179,14 @@ impl Ppu {
// t: ...GH.. ........ <- d: ......GH
// <used elsewhere> <- d: ABCDEF..
self.t = (self.t & 0xf3ff) | ((val as u16 & 0x3) << 10);
},
}
Register::Mask2001 => {
self.show_background_left = val & 0x02 == 0x02;
self.show_sprites_left = val & 0x04 == 0x04;
self.show_background = val & 0x08 == 0x08;
self.show_sprites = val & 0x10 == 0x10;
self.rendering_enabled = self.show_background || self.show_sprites;
},
}
Register::OamAddr2003 => self.oam_address = val,
Register::OamData2004 => {
self.oam[self.oam_address as usize] = val;
@ -197,7 +207,7 @@ impl Ppu {
}
self.w_latch = !self.w_latch;
},
}
Register::Addr2006 => {
if self.w_latch {
let cdefgh = val & 0x3f;
@ -208,7 +218,7 @@ impl Ppu {
self.t |= val as u16;
self.v = self.t;
}
self.w_latch = !self.w_latch;
}
Register::Data2007 => {
@ -218,11 +228,11 @@ impl Ppu {
0x2000..=0x2fff => self.vram.write(val, address),
0x3000..=0x3eff => self.vram.write(val, address - 0x1000),
0x3f00..=0x3fff => self.palette.write(val, address),
_ => (), //panic!("invalid write: {:#06x} for {:?}", address, kind)
_ => (), //panic!("invalid write: {:#06x} for {:?}", address, kind)
}
self.inc_v();
}
_ => ()
_ => (),
}
}
@ -238,7 +248,9 @@ impl Ppu {
self.sprite_overflow = false;
}
(Phase::Render | Phase::PreRender, 256, Rendering::Enabled) => self.inc_y(),
(Phase::Render | Phase::PreRender, 257, Rendering::Enabled) => self.copy_horizontal_from_t_to_v(),
(Phase::Render | Phase::PreRender, 257, Rendering::Enabled) => {
self.copy_horizontal_from_t_to_v()
}
(Phase::PreRender, 280..=304, Rendering::Enabled) => self.copy_vertical_from_t_to_v(),
(Phase::Render, 0..=255, _) => {
// Visible pixels
@ -254,7 +266,7 @@ impl Ppu {
if sprites_visible {
self.render_sprite_pixel(x, y, bg_pixel_drawn);
}
},
}
(Phase::Render, 320, _) => {
// Load sprites for next line (sprite tile loading interval)
// https://www.nesdev.org/wiki/PPU_rendering#Cycles_257-320
@ -264,16 +276,20 @@ impl Ppu {
(Phase::EnteringVblank, 1, _) => self.in_vblank = true,
(Phase::Render | Phase::PostRender, 260, Rendering::Enabled) => {
irq = self.rom_mapper.borrow_mut().irq()
},
}
_ => (),
}
trace!(
Tag::PpuTiming,
"clock: {}, cycle: {}, scanline: {}, vblank: {}, nmi: {}",
self.state.clock(), self.state.cycle(), self.state.scanline(), self.in_vblank, self.nmi_at_start_of_vblank
Tag::PpuTiming,
"clock: {}, cycle: {}, scanline: {}, vblank: {}, nmi: {}",
self.state.clock(),
self.state.cycle(),
self.state.scanline(),
self.in_vblank,
self.nmi_at_start_of_vblank
);
};
}
if !vblank_pre_ticks && self.in_vblank {
TickEvent::EnteredVblank
@ -287,7 +303,7 @@ impl Ppu {
fn render_background_pixel(&mut self, x: usize, y: usize) -> bool {
let v = self.v;
let fine_x = self.fine_x as u16;
let scroll_x = (v & 0x1f) * 8 + fine_x;
let scroll_y = ((v >> 5) & 0x1f) * 8 + (v >> 12);
@ -309,8 +325,10 @@ impl Ppu {
let horizontal_tile: u16 = virtual_x / 8;
let nametable_offset = vertical_tile * 32 + horizontal_tile;
let nametable_entry = self.vram.read_indexed(virtual_nametable_index, nametable_offset);
let nametable_entry = self
.vram
.read_indexed(virtual_nametable_index, nametable_offset);
let vertical_attr = vertical_tile / 4;
let horizontal_attr = horizontal_tile / 4;
@ -322,8 +340,12 @@ impl Ppu {
let color_bits = (attr >> ((horizontal_box_pos * 2) + (vertical_box_pos * 4))) & 0x3;
let first_plane_byte = self.read_chr_rom(self.background_table_address + (nametable_entry as u16 * 0x10 + virtual_y % 8));
let second_plane_byte = self.read_chr_rom(self.background_table_address + (nametable_entry as u16 * 0x10 + (virtual_y % 8) + 8));
let first_plane_byte = self.read_chr_rom(
self.background_table_address + (nametable_entry as u16 * 0x10 + virtual_y % 8),
);
let second_plane_byte = self.read_chr_rom(
self.background_table_address + (nametable_entry as u16 * 0x10 + (virtual_y % 8) + 8),
);
let first_plane_bit = first_plane_byte >> (7 - virtual_x % 8) & 0x1;
let second_plane_bit = second_plane_byte >> (7 - virtual_x % 8) & 0x1;
@ -333,8 +355,7 @@ impl Ppu {
let rgb = self.palette.rgb_from_index(0);
self.frame.set_pixel_xy(x, y, rgb);
false
}
else {
} else {
let palette_entry = first_plane_bit + (second_plane_bit * 2) + (color_bits * 4);
let rgb = self.palette.rgb_from_index(palette_entry);
self.frame.set_pixel_xy(x, y, rgb);
@ -362,7 +383,8 @@ impl Ppu {
}
// https://www.nesdev.org/wiki/PPU_OAM#Sprite_zero_hits
let s0_hit_disabled = x == 255 || (x <= 7 && (!self.show_sprites_left || !self.show_background_left));
let s0_hit_disabled =
x == 255 || (x <= 7 && (!self.show_sprites_left || !self.show_background_left));
if !s0_hit_disabled && !self.sprite_0_hit && sprite.zero && bg_pixel_drawn {
self.sprite_0_hit = true;
@ -379,7 +401,8 @@ impl Ppu {
let next_line = self.state.scanline() as u8 + 1;
let mut sprite_n = 0;
for sprite in 0..64 { // there's 64 sprites in total
for sprite in 0..64 {
// there's 64 sprites in total
let sprite_addr = sprite * 4; // each sprite is 4 bytes
// https://www.nesdev.org/wiki/PPU_OAM
@ -405,7 +428,7 @@ impl Ppu {
let attr = self.oam[sprite_addr + 2];
let x = self.oam[sprite_addr + 3];
let vflip = (attr & 0x80) == 0x80;
let tile_row = match vflip {
true => sprite_height - 1 - (next_line - sprite_y),
@ -437,7 +460,7 @@ impl Ppu {
pixels,
priority: (attr & 0x20) == 0x20,
x,
zero: sprite_n == 0
zero: sprite_n == 0,
});
sprite_n += 1;
@ -530,4 +553,4 @@ impl Ppu {
pub fn nmi_on_vblank(&self) -> bool {
self.nmi_at_start_of_vblank
}
}
}

View file

@ -1,13 +1,17 @@
#[derive(Default, PartialEq, Eq, Copy, Clone)]
pub(crate) enum Phase {
PreRender,
#[default] Render,
#[default]
Render,
PostRender,
EnteringVblank,
Vblank
Vblank,
}
pub(crate) enum Rendering { Enabled, Disabled }
pub(crate) enum Rendering {
Enabled,
Disabled,
}
#[derive(Default)]
pub(crate) struct State {
@ -30,7 +34,7 @@ impl State {
240 => Phase::PostRender,
241 => Phase::EnteringVblank,
242..=260 => Phase::Vblank,
_ => unreachable!()
_ => unreachable!(),
};
if self.phase == Phase::PreRender {
@ -45,9 +49,13 @@ impl State {
self.odd_frame = !self.odd_frame;
(
self.phase,
self.cycle,
if rendering_enabled { Rendering::Enabled } else { Rendering::Disabled }
self.phase,
self.cycle,
if rendering_enabled {
Rendering::Enabled
} else {
Rendering::Disabled
},
)
}
@ -67,4 +75,4 @@ impl State {
pub fn clock(&self) -> usize {
self.clock
}
}
}

View file

@ -1,8 +1,11 @@
use core::cell::{RefCell};
use alloc::rc::Rc;
use common::kilobytes;
use alloc::boxed::Box;
use crate::{cartridge::Mirroring, mappers::Mapper};
use alloc::rc::Rc;
use core::cell::RefCell;
use common::kilobytes;
use crate::cartridge::Mirroring;
use crate::mappers::Mapper;
pub(crate) struct Vram {
nametables: [[u8; kilobytes::KB1]; 2], // AKA CIRAM
@ -15,13 +18,15 @@ impl Vram {
let mirror_map = Rc::new(RefCell::new(mirror_map));
let m = mirror_map.clone();
mapper.borrow_mut().on_runtime_mirroring(Box::new( move |new_mirroring| {
*m.borrow_mut() = Self::setup_mirror_map(new_mirroring);
}));
mapper
.borrow_mut()
.on_runtime_mirroring(Box::new(move |new_mirroring| {
*m.borrow_mut() = Self::setup_mirror_map(new_mirroring);
}));
Self {
Self {
nametables: [[0; kilobytes::KB1]; 2],
mirror_map
mirror_map,
}
}
@ -33,12 +38,16 @@ impl Vram {
Mirroring::Horizontal => [0, 0, 1, 1],
Mirroring::SingleScreenLower => [0, 0, 0, 0],
Mirroring::SingleScreenUpper => [1, 1, 1, 1],
_ => panic!()
_ => panic!(),
}
}
pub fn read(&self, address: u16) -> u8 {
assert!((0x2000..=0x2fff).contains(&address), "Invalid vram read: {:#06x}", address);
assert!(
(0x2000..=0x2fff).contains(&address),
"Invalid vram read: {:#06x}",
address
);
let virtual_index = Self::get_virtual_nametable_index(address);
let index = self.mirror_map.borrow()[virtual_index] as usize;
@ -47,7 +56,11 @@ impl Vram {
}
pub fn write(&mut self, val: u8, address: u16) {
assert!((0x2000..=0x2fff).contains(&address), "Invalid vram write: {:#06x}", address);
assert!(
(0x2000..=0x2fff).contains(&address),
"Invalid vram write: {:#06x}",
address
);
let virtual_index = Self::get_virtual_nametable_index(address);
let index = self.mirror_map.borrow()[virtual_index] as usize;
@ -69,7 +82,7 @@ impl Vram {
#[cfg(test)]
mod tests {
use crate::{ppu::vram::Vram};
use crate::ppu::vram::Vram;
#[test]
fn get_virtual_nametable_index() {
@ -79,4 +92,4 @@ mod tests {
assert_eq!(Vram::get_virtual_nametable_index(0x2c00), 3);
assert_eq!(Vram::get_virtual_nametable_index(0x24ff), 1);
}
}
}

View file

@ -8,17 +8,26 @@ const STATUS_SUCCESS: u8 = 0x00;
const VALID_MAGIC: [u8; 3] = [0xde, 0xb0, 0x61];
#[derive(PartialEq, Eq)]
enum PassCond { Status(&'static str, u8), Pc(u16) }
enum PassCond {
Status(&'static str, u8),
Pc(u16),
}
#[test]
fn instr_test_v5_official_mmc1() {
run_blargg_test("instr_test-v5/official_only.nes", PassCond::Status("All 16 tests passed", STATUS_SUCCESS))
run_blargg_test(
"instr_test-v5/official_only.nes",
PassCond::Status("All 16 tests passed", STATUS_SUCCESS),
)
}
#[test]
fn instr_test_v5_basic_nrom() {
// run_blargg_test("instr_test-v5/rom_singles/01-basics.nes", PassCond::Status("01-basics\n\nPassed", STATUS_SUCCESS));
run_blargg_test("instr_test-v5/rom_singles/01-basics.nes", PassCond::Pc(0x01e2));
run_blargg_test(
"instr_test-v5/rom_singles/01-basics.nes",
PassCond::Pc(0x01e2),
);
}
#[test]
@ -33,45 +42,73 @@ fn instr_misc() {
fn ppu_vbl_nmi() {
let success = "VBL period is too long with BG off\n\n01-vbl_basics\n\nFailed #8";
// let success = "$2002 should be mirrored at $200A\n\n01-vbl_basics\n\nFailed #5";
run_blargg_test("ppu_vbl_nmi/rom_singles/01-vbl_basics.nes", PassCond::Status(success, 0x08));
run_blargg_test(
"ppu_vbl_nmi/rom_singles/01-vbl_basics.nes",
PassCond::Status(success, 0x08),
);
}
#[ignore = "not implemented"]
#[test]
fn ppu_open_bus() {
let success = "Bits 2-4 of sprite attributes should always be clear when read\n\nppu_open_bus\n\nFailed #10";
run_blargg_test("ppu_open_bus/ppu_open_bus.nes", PassCond::Status(success, 0x0a));
let success =
"Bits 2-4 of sprite attributes should always be clear when read\n\nppu_open_bus\n\nFailed #10";
run_blargg_test(
"ppu_open_bus/ppu_open_bus.nes",
PassCond::Status(success, 0x0a),
);
}
#[ignore = "bad test"]
#[test]
fn cpu_exec_space() {
let success = "\u{1b}[0;37mTEST:test_cpu_exec_space_ppuio\n\u{1b}[0;33mThis program verifies that the\nCPU can execute code from any\npossible location that it can\naddress, including I/O space.\n\nIn addition, it will be tested\nthat an RTS instruction does a\ndummy read of the byte that\nimmediately follows the\ninstructions.\n\n\u{1b}[0;37m\u{1b}[1;34mJSR+RTS TEST OK\nJMP+RTS TEST OK\nRTS+RTS TEST OK\nJMP+RTI TEST OK\nJMP+BRK TEST OK\n\u{1b}[0;37m\nPassed";
run_blargg_test("cpu_exec_space/test_cpu_exec_space_ppuio.nes", PassCond::Status(success, 0x00));
run_blargg_test(
"cpu_exec_space/test_cpu_exec_space_ppuio.nes",
PassCond::Status(success, 0x00),
);
}
#[test]
fn branch_timing() {
run_blargg_test("branch_timing_tests/1.Branch_Basics.nes", PassCond::Pc(0xe01d));
run_blargg_test("branch_timing_tests/2.Backward_Branch.nes", PassCond::Pc(0xe01d));
run_blargg_test("branch_timing_tests/3.Forward_Branch.nes", PassCond::Pc(0xe01d));
run_blargg_test(
"branch_timing_tests/1.Branch_Basics.nes",
PassCond::Pc(0xe01d),
);
run_blargg_test(
"branch_timing_tests/2.Backward_Branch.nes",
PassCond::Pc(0xe01d),
);
run_blargg_test(
"branch_timing_tests/3.Forward_Branch.nes",
PassCond::Pc(0xe01d),
);
}
#[test]
fn palette_ram() {
run_blargg_test("blargg_ppu_tests_2005.09.15b/palette_ram.nes", PassCond::Pc(0xe0eb));
run_blargg_test(
"blargg_ppu_tests_2005.09.15b/palette_ram.nes",
PassCond::Pc(0xe0eb),
);
}
#[test]
#[ignore = "bad test"]
fn oven_odd_frames() {
run_blargg_test("ppu_vbl_nmi/rom_singles/09-even_odd_frames.nes", PassCond::Status("dunno", STATUS_SUCCESS));
run_blargg_test(
"ppu_vbl_nmi/rom_singles/09-even_odd_frames.nes",
PassCond::Status("dunno", STATUS_SUCCESS),
);
}
#[test]
#[ignore = "bad test"]
fn ppu_read_buffer() {
run_blargg_test("ppu_read_buffer/test_ppu_read_buffer.nes", PassCond::Status("dunno", STATUS_SUCCESS));
run_blargg_test(
"ppu_read_buffer/test_ppu_read_buffer.nes",
PassCond::Status("dunno", STATUS_SUCCESS),
);
}
fn run_blargg_test(test: &str, pass_condition: PassCond) {
@ -81,9 +118,11 @@ fn run_blargg_test(test: &str, pass_condition: PassCond) {
let result: String;
let mut status: Option<u8> = None;
nes.debugger().watch_memory_range(0x6004..=0x6004+100, |mem| {
println!("{}", read_null_terminated_string(&mem));
});
nes
.debugger()
.watch_memory_range(0x6004..=0x6004 + 100, |mem| {
println!("{}", read_null_terminated_string(&mem));
});
loop {
nes.tick();
@ -97,12 +136,13 @@ fn run_blargg_test(test: &str, pass_condition: PassCond) {
match status {
Some(STATUS_RUNNING) => (),
Some(STATUS_NEEDS_RESET) => panic!("needs reset.."),
Some(0x00..=0x7F) => { // Completed, status is the result code.
let mem_view = nes.bus().read_range(0x6004..=0x6004+1000);
Some(0x00..=0x7F) => {
// Completed, status is the result code.
let mem_view = nes.bus().read_range(0x6004..=0x6004 + 1000);
result = read_null_terminated_string(&mem_view);
break
},
_ => panic!("unknown status")
break;
}
_ => panic!("unknown status"),
}
}
}
@ -111,8 +151,7 @@ fn run_blargg_test(test: &str, pass_condition: PassCond) {
println!("status code: {:#04x}", status.unwrap());
assert_eq!(success_str, result.trim());
assert_eq!(Some(success_status), status);
}
else {
} else {
unreachable!()
}
}
@ -132,4 +171,4 @@ fn check_and_update_status(nes: &Nes, current_status: &mut Option<u8>) -> bool {
}
}
false
}
}

View file

@ -1,10 +1,11 @@
use std::path::PathBuf;
use nes::{cartridge::Cartridge, nes::Nes};
use nes::cartridge::Cartridge;
use nes::nes::Nes;
pub fn setup(path: PathBuf, verbose: bool) -> Nes {
let cartridge = Cartridge::blow_dust(path).expect("failed to map rom");
let mut nes = Nes::insert_headless_host(cartridge);
nes.debugger().verbose(verbose);
nes
}
}

View file

@ -1,5 +1,10 @@
use mos6502::cpu::{Flag, Reg};
use std::{fmt::Write, fs::File, io::{BufReader, BufRead}};
use std::fmt::Write;
use std::fs::File;
use std::io::BufRead;
use std::io::BufReader;
use mos6502::cpu::Flag;
use mos6502::cpu::Reg;
mod common;
@ -21,7 +26,8 @@ const ENABLE_TEST_CYCLES: bool = false;
fn nestest() {
let mut nes = common::setup("../test-roms/nestest/nestest.nes".into(), false);
let logf = File::open("../test-roms/nestest/nestest_cycles.log").expect("failed to read test log");
let logf =
File::open("../test-roms/nestest/nestest_cycles.log").expect("failed to read test log");
let log: Vec<String> = BufReader::new(logf).lines().map(|s| s.unwrap()).collect();
// nes.machine().debugger().enable();
@ -35,10 +41,18 @@ fn nestest() {
nes.cpu_mut()[Flag::I] = 1;
nes.cpu_mut()[Reg::SP] = 0xfd;
nes.debugger().watch_memory_range(NESTEST_RES_BYTE2..=NESTEST_RES_BYTE3, |result| {
assert_eq!(result[0], 0x00, "nestest reports error code on byte 2.. check README");
assert_eq!(result[1], 0x00, "nestest reports error code on byte 3.. check README");
});
nes
.debugger()
.watch_memory_range(NESTEST_RES_BYTE2..=NESTEST_RES_BYTE3, |result| {
assert_eq!(
result[0], 0x00,
"nestest reports error code on byte 2.. check README"
);
assert_eq!(
result[1], 0x00,
"nestest reports error code on byte 3.. check README"
);
});
let mut i = 0;
loop {
@ -48,9 +62,12 @@ fn nestest() {
nes.tick();
if log[i] != sts && ENABLE_TEST_CYCLES {
nes.debugger().dump_backtrace();
panic!("nestest cycle test mismatch!\n\nExpected:\t{}\nActual:\t\t{}\n", log[i], sts);
panic!(
"nestest cycle test mismatch!\n\nExpected:\t{}\nActual:\t\t{}\n",
log[i], sts
);
}
i += 1;
if nes.cpu().pc() == NESTEST_SUCCESS {
@ -60,4 +77,4 @@ fn nestest() {
let expected_ticks = 8980;
assert_eq!(expected_ticks, nes.cpu_ticks());
}
}

View file

@ -5,7 +5,9 @@
extern crate alloc;
use alloc::vec::Vec;
use nes::{cartridge::Cartridge, nes::Nes};
use nes::cartridge::Cartridge;
use nes::nes::Nes;
#[cfg(feature = "profile_heap")]
#[global_allocator]
@ -20,9 +22,7 @@ impl nes::nes::HostPlatform for FakeHost {
0
}
fn delay(&self, _: core::time::Duration) {
}
fn delay(&self, _: core::time::Duration) {}
#[no_mangle]
fn render(&mut self, f: &nes::frame::RenderFrame) {
@ -51,7 +51,7 @@ fn main() {
let rom = include_bytes!(env!("PROF_ROM"));
let cart = Cartridge::blow_dust_no_heap(rom).unwrap();
let mut nes = Nes::insert(cart, FakeHost{});
let mut nes = Nes::insert(cart, FakeHost {});
for _ in 0..10_000_000 {
nes.tick();