mirror of
https://github.com/henrikpersson/potatis.git
synced 2024-05-20 05:10:17 -04:00
rustfmt it all
This commit is contained in:
parent
7c49ab8807
commit
b9ccc3fe8e
7
.rustfmt.toml
Normal file
7
.rustfmt.toml
Normal 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
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
_ => (),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
|||
}
|
||||
}};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
_ => ()
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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> {
|
|||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
_ => ()
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
127
nes/src/nes.rs
127
nes/src/nes.rs
|
@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
mod palette;
|
||||
mod vram;
|
||||
mod state;
|
||||
mod vram;
|
||||
|
||||
pub(crate) mod ppu;
|
||||
pub(crate) mod ppu;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in a new issue