Compare commits

...

16 commits

Author SHA1 Message Date
Henry Sloan
cb1742e26f
Merge pull request #23 from henryksloan/audio-sync-fix
Improve audio sync with basic dynamic sampling
2023-08-20 13:07:03 -07:00
Henry Sloan
259c3265fa Add simple volume attenuation 2023-08-20 12:58:39 -07:00
Henry Sloan
0228c3a94a Improve audio sync with basic dynamic sampling 2023-08-20 12:43:09 -07:00
Henry Sloan
09cf4c2628
Merge pull request #22 from qeeg/main
Fix a Rust 2021 warning
2021-05-15 22:10:44 -04:00
qeeg
d8967f216c Fix a Rust 2021 warning 2021-05-15 14:49:58 -05:00
Henry Sloan
f23628a638 Hotfix for playing with no controller 2021-04-18 18:17:34 -04:00
Henry Sloan
1d792bf9af
Merge pull request #21 from henryksloan/ui-improvements
Gamepad support and UI improvements
2021-04-03 18:20:04 -04:00
Henry Sloan
af3acf8c85 Update README 2021-04-03 18:10:22 -04:00
Henry Sloan
54a628bbf9 Make iNES parsing more robust, and add mapper 71 2021-04-03 18:04:32 -04:00
Henry Sloan
27cadf6f5f Change NT and APU logic so 4-screen games work 2021-03-31 14:57:37 -04:00
Henry Sloan
6512bf1315 Fix minor mapper bugs 2021-03-31 14:09:56 -04:00
Henry Sloan
627dd2d70f Add NMI delay and more unofficial instructions 2021-03-31 11:43:29 -04:00
Henry Sloan
14a4135f5b Add D-Pad support and made loop more extensible 2021-03-28 13:01:18 -04:00
Henry Sloan
5eff260ac1 Add controller support 2021-03-28 12:45:01 -04:00
Henry Sloan
63dbaa1719 Add pause feature 2021-03-28 10:52:39 -04:00
Henry Sloan
bcf67d12c8
Add download instructions to README 2021-03-26 16:05:59 -04:00
17 changed files with 437 additions and 120 deletions

View file

@ -4,18 +4,16 @@ KindNES is a reasonably accurate NES emulator written in Rust. It strives for po
### Usage
Either give a .NES ROM file as a command line argument, or use the File > Open ROM menu bar option (currently only on the Windows version).
| Button | Key |
| --- | --- |
| D-Pad | Arrow keys |
| A button | X |
| B button | Z |
| Start | Enter |
| Select | Right shift |
| Button | Key | Gamepad |
| --- | --- | --- |
| D-Pad | Arrow keys | D-Pad or joystick |
| A button | X | A (Playstation X) |
| B button | Z | B (Playstation O) |
| Start | Enter | Start |
| Select | Right shift | Select |
# Downloads
*WIP*
Downloads will be available via Github releases as soon as all platforms have a packaged MVP.
Downloads can be found on the [releases page](https://github.com/henryksloan/kind-nes/releases). Currently, only the Windows version is packaged, so users of other platforms should build KindNES as described below. There are plans for Linux (AppImage) packages in the near future, and eventually MacOS packages.
# Building
## Dependencies
@ -52,7 +50,6 @@ KindNES supports most of the common NES mappers, meaning that it supports the ma
## Next steps
- Improved UI
- More menubar features
- Pause, (soft/hard) reset
- An improved cross-platform UI with the same menubar features as the Windows version
- First priority: Centralize basic features like [file dialogs](https://github.com/EmbarkStudios/nfd2) to sdl-ui shortcuts
- Still looking for a GUI framework with great menubar support and SDL2 integration
@ -65,7 +62,6 @@ KindNES supports most of the common NES mappers, meaning that it supports the ma
- First, get perfect FPS control (it currently sleeps slightly too long at the end of frames)
- Variable audio sample rate
- Controls
- Gamepad support
- Modifiable controls
- Local multiplayer?
@ -84,5 +80,6 @@ KindNES supports most of the common NES mappers, meaning that it supports the ma
- Sound channel mixer
- Step-in debugger
- A GDB-style command prompt would be awesome
- Distinguish between hard and soft reset
- Cheats
- TAS creation
- TAS creation

View file

@ -24,6 +24,7 @@ pub struct APU {
frame_counter_cycle: u64,
frame_sequence_len: u8,
frame_sequence_step: u8,
bus_latch: u8,
irq_disable: bool,
frame_irq: bool,
@ -48,6 +49,7 @@ impl APU {
frame_counter_cycle: 0,
frame_sequence_len: 4,
frame_sequence_step: 0,
bus_latch: 0,
irq_disable: false,
frame_irq: false,
@ -89,6 +91,7 @@ impl APU {
self.frame_counter_cycle = 0;
self.frame_sequence_len = 4;
self.frame_sequence_step = 0;
self.bus_latch = 0;
self.dmc.irq = false;
self.irq_disable = false;
@ -166,15 +169,20 @@ impl APU {
// https://wiki.nesdev.com/w/index.php/APU_registers
impl Memory for APU {
fn read(&mut self, addr: u16) -> u8 {
assert!(addr == 0x4015);
if addr != 0x4015 {
return 0;
}
let data = self.peek(addr);
self.frame_irq = false;
self.bus_latch = data;
data
}
fn peek(&self, addr: u16) -> u8 {
assert!(addr == 0x4015);
if addr != 0x4015 {
return 0;
}
((self.dmc.irq as u8) << 7)
| ((self.frame_irq as u8) << 6)
@ -188,6 +196,7 @@ impl Memory for APU {
fn write(&mut self, addr: u16, data: u8) {
assert!((0x4000 <= addr && addr <= 0x4017) && addr != 0x4014);
self.bus_latch = data;
if 0x4000 <= addr && addr <= 0x4003 {
self.pulse1.update_register(addr - 0x4000, data);
} else if 0x4004 <= addr && addr <= 0x4007 {

View file

@ -88,7 +88,6 @@ lazy_static! {
add("TYA", vec![(0x98, IMP, 2)]);
// https://wiki.nesdev.com/w/index.php/CPU_unofficial_opcodes
// TODO: http://visual6502.org/wiki/index.php?title=6502_Opcode_8B_%28XAA,_ANE%29
add("*NOP", vec![(0x80, IMM, 2),
(0x82, IMM, 2), (0xC2, IMM, 2), (0xE2, IMM, 2),
(0x04, ZER, 3), (0x44, ZER, 3), (0x64, ZER, 3),
@ -115,7 +114,7 @@ lazy_static! {
add("*ARR", vec![(0x6B, IMM, 2)]);
add("*SAX", vec![(0x83, INX, 6), (0x87, ZER, 3), (0x8F, ABS, 4), (0x97, ZEY, 4)]);
add("*SBC", vec![(0xEB, IMM, 2)]);
add("*LAX", vec![(0xA3, INX, 6), (0xA7, ZER, 3), (0xAB, IMM, 4), (0xAF, ABS, 4),
add("*LAX", vec![(0xA3, INX, 6), (0xA7, ZER, 3), (0xAB, IMM, 2), (0xAF, ABS, 4),
(0xB3, INY, 5), (0xB7, ZEY, 4), (0xBF, ABY, 4)]);
add("*LAS", vec![(0xBB, ABY, 4)]);
add("*DCP", vec![(0xC3, INX, 8), (0xC7, ZER, 5), (0xCF, ABS, 6), (0xD3, INY, 8),
@ -127,6 +126,8 @@ lazy_static! {
add("*SHY", vec![(0x9C, ABX, 5)]);
add("*AXA", vec![(0x93, INY, 6), (0x9F, ABX, 5)]);
add("*AXS", vec![(0xCB, IMM, 2)]);
add("*XAA", vec![(0x8B, IMM, 2)]);
add("*TAS", vec![(0x9B, ABY, 5)]);
map
};
}

View file

@ -35,6 +35,7 @@ pub struct CPU {
wait_cycles: u32,
cycles: u64,
pub log: bool,
pub nmi_timer: u8,
memory: Box<dyn Memory>,
}
@ -51,10 +52,12 @@ impl CPU {
wait_cycles: 0,
cycles: 0,
log: false,
nmi_timer: 0,
memory,
}
}
// TODO: "Reset should set I flag, subtract 3 from S, nothing more."
pub fn reset(&mut self) {
self.a = 0;
self.x = 0;
@ -63,6 +66,7 @@ impl CPU {
self.p = StatusRegister::from_bits(0b00100100).unwrap();
self.wait_cycles = 0;
self.cycles = 7;
self.nmi_timer = 0;
self.pc = self.memory.read_u16(RST_VEC);
}
@ -83,6 +87,13 @@ impl CPU {
}
pub fn step(&mut self) -> Option<String> {
if self.nmi_timer > 0 {
self.nmi_timer -= 1;
if self.nmi_timer == 0 {
self.nmi();
}
}
let opcode = self.memory.read(self.pc);
let op = INSTRUCTIONS
.get(&opcode)
@ -314,24 +325,30 @@ impl CPU {
"TYA" => self.a = self.transfer_op(self.y, false),
// https://wiki.nesdev.com/w/index.php/Programming_with_unofficial_opcodes
// Combined instructions:
"*NOP" => {
if self.memory.peek(self.pc - 1) & 0xF == 0xC && self.get_operand_address(mode).1 {
self.wait_cycles += 1;
}
}
"*XAA" => self.xaa(mode),
"*TAS" => {
self.s = self.a & self.x;
self.store_high_op(self.s, mode);
}
// Combined instructions:
"*ALR" => self.combined_op(vec![0x29, 0x4A]),
"*ANC" => self.anc(),
"*ARR" => self.arr(&mode),
"*AXS" => self.axs(&mode),
"*ARR" => self.arr(mode),
"*AXS" => self.axs(mode),
"*LAX" => self.combined_op_str(vec!["LDA", "TAX"], mode, true),
"*LAS" => self.las(mode),
"*SAX" => {
let addr = self.get_operand_address(mode).0;
self.memory.write(addr, self.a & self.x);
}
"*SHY" => self.store_high_op(self.y, &mode),
"*SHX" => self.store_high_op(self.x, &mode),
"*AXA" => self.store_high_op(self.a & self.x, &mode),
"*SHY" => self.store_high_op(self.y, mode),
"*SHX" => self.store_high_op(self.x, mode),
"*AXA" => self.store_high_op(self.a & self.x, mode),
// RMW instructions
"*DCP" => self.combined_op_str(vec!["DEC", "CMP"], mode, false),
@ -418,7 +435,11 @@ impl CPU {
/// Store data from a register into memory
fn store_op(&mut self, reg: u8, mode: &AddressingMode) {
let (addr, _) = self.get_operand_address(mode);
let (addr, page_cross) = self.get_operand_address(mode);
if !page_cross && (*mode == AddressingMode::ABY) {
// TODO: This makes ppu_read_buffer test work better, but verify this
self.memory.read(addr); // Dummy read
}
self.memory.write(addr, reg);
}
@ -628,6 +649,31 @@ impl CPU {
self.p.set(StatusRegister::NEGATIVE, (self.x & 0x80) != 0);
self.p.set(StatusRegister::ZERO, self.x == 0);
}
/// Unofficial: Load A, X, and S with (low byte of the address) & S
fn las(&mut self, mode: &AddressingMode) {
let (addr, page_cross) = self.get_operand_address(mode);
let _ = self.memory.read(addr);
let val = (addr as u8) & self.s;
self.a = val;
self.x = val;
self.s = val;
self.p.set(StatusRegister::NEGATIVE, (self.a & 0x80) != 0);
self.p.set(StatusRegister::ZERO, self.a == 0);
if page_cross {
self.wait_cycles += 1;
}
}
/// Unofficial: http://visual6502.org/wiki/index.php?title=6502_Opcode_8B_%28XAA,_ANE%29
fn xaa(&mut self, mode: &AddressingMode) {
let (addr, _) = self.get_operand_address(mode);
let data = self.memory.read(addr);
self.a = self.a & self.x & data;
self.p.set(StatusRegister::NEGATIVE, (self.a & 0x80) != 0);
self.p.set(StatusRegister::ZERO, self.a == 0);
}
}
impl Memory for CPU {

View file

@ -24,9 +24,6 @@ impl Memory for ROM {
}
fn write(&mut self, addr: u16, data: u8) {
panic!(format!(
"attempted to write {:X} to address {:X} in ROM",
data, addr
))
panic!("attempted to write {:X} to address {:X} in ROM", data, addr)
}
}

View file

@ -1,5 +1,7 @@
// https://wiki.nesdev.com/w/index.php/INES
// https://wiki.nesdev.com/w/index.php/NES_2.0#Header
pub struct CartridgeMetadata {
pub is_nes2: bool,
pub n_prg_banks: u16,
pub n_chr_banks: u16,
pub hardwired_mirroring: Mirroring,
@ -8,28 +10,28 @@ pub struct CartridgeMetadata {
pub mapper_num: u16,
// Uncommon features
pub submapper_num: u16,
pub submapper_num: Option<u16>,
pub console_type: ConsoleType,
pub prg_ram_shifts: u8,
pub prg_nvram_shifts: u8,
pub chr_ram_shifts: u8,
pub chr_nvram_shifts: u8,
pub prg_ram_bytes: usize,
pub prg_nvram_bytes: usize,
pub chr_ram_bytes: usize,
pub chr_nvram_bytes: usize,
pub timing: ClockTiming,
pub vs_system_type: u8,
pub extended_console_type: u8,
pub n_misc_roms: u8,
pub default_expansion_device: u8,
pub vs_system_type: Option<u8>,
pub n_misc_roms: Option<u8>,
pub default_expansion_device: Option<u8>,
}
impl CartridgeMetadata {
pub fn from_header(header: Vec<u8>) -> Result<Self, &'static str> {
// TODO: Some garbage data in iNES 1.0 headers breaks this parsing
if header[0..=3] != [b'N', b'E', b'S', 0x1A] {
return Err("header does not begin with NES<EOF> identifier");
}
let n_prg_banks = (((header[9] & 0x0F) as u16) << 8) | (header[4] as u16);
let n_chr_banks = (((header[9] & 0xF0) as u16) << 4) | (header[5] as u16);
let is_nes2 = (header[7] & 0b1100) == 0b1000;
let mut n_prg_banks = header[4] as u16;
let mut n_chr_banks = header[5] as u16;
let hardwired_mirroring = if ((header[6] >> 3) & 1) == 1 {
Mirroring::FourScreen
@ -39,42 +41,108 @@ impl CartridgeMetadata {
_ => Mirroring::Vertical,
}
};
let has_battery = ((header[6] >> 1) & 1) == 1;
let has_trainer = ((header[6] >> 2) & 1) == 1;
let mut mapper_num = (header[6] >> 4) as u16;
let mapper_num = ((header[6] >> 4) as u16)
| ((header[7] & 0xF0) as u16)
| (((header[8] & 0x0F) as u16) << 8);
let submapper_num = (header[8] >> 4) as u16;
let mut submapper_num = None;
let mut timing = ClockTiming::NTSC;
let mut vs_system_type = None;
let mut n_misc_roms = None;
let mut default_expansion_device = None;
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
2 => ConsoleType::Playchoice10,
_ => ConsoleType::Extended,
};
let (console_type, prg_ram_bytes, prg_nvram_bytes, chr_ram_bytes, chr_nvram_bytes) =
if is_nes2 {
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
2 => ConsoleType::Playchoice10,
_ => match header[13] & 0b1111 {
0x0 => ConsoleType::NESFamicom,
0x1 => ConsoleType::VsSystem,
0x2 => ConsoleType::Playchoice10,
0x3 => ConsoleType::DecimalFamiclone,
0x4 => ConsoleType::VT01Monochrome,
0x5 => ConsoleType::VT01STN,
0x6 => ConsoleType::VT02,
0x7 => ConsoleType::VT03,
0x8 => ConsoleType::VT09,
0x9 => ConsoleType::VT32,
0xA => ConsoleType::VT369,
0xB => ConsoleType::UM6578,
_ => ConsoleType::Other,
},
};
let prg_ram_shifts = header[10] & 0x0F;
let prg_nvram_shifts = (header[10] & 0xF0) >> 4;
let chr_ram_shifts = header[11] & 0x0F;
let chr_nvram_shifts = (header[11] & 0xF0) >> 4;
mapper_num |= ((header[7] & 0xF0) as u16) | (((header[8] & 0x0F) as u16) << 8);
submapper_num = Some((header[8] >> 4) as u16);
let timing = match header[12] & 0b11 {
0 => ClockTiming::NTSC,
1 => ClockTiming::PAL,
2 => ClockTiming::MultiRegion,
_ => ClockTiming::Dendy,
};
n_prg_banks |= ((header[9] & 0x0F) as u16) << 8;
n_chr_banks |= ((header[9] & 0xF0) as u16) << 4;
// TODO: These can be enums
let vs_system_type = header[13];
let extended_console_type = header[13] & 0x0F;
let prg_ram_bytes = 64usize << (header[10] & 0x0F);
let prg_nvram_bytes = 64usize << ((header[10] & 0xF0) >> 4);
let chr_ram_bytes = 64usize << (header[11] & 0x0F);
let chr_nvram_bytes = 64usize << ((header[11] & 0xF0) >> 4);
let n_misc_roms = header[14] & 0b11;
let default_expansion_device = header[14] & 0b11_1111;
timing = match header[12] & 0b11 {
0 => ClockTiming::NTSC,
1 => ClockTiming::PAL,
2 => ClockTiming::MultiRegion,
_ => ClockTiming::Dendy,
};
// This could be an enum if it's ever used
vs_system_type = Some(header[13]);
n_misc_roms = Some(header[14] & 0b11);
default_expansion_device = Some(header[14] & 0b11_1111);
(
console_type,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
chr_nvram_bytes,
)
} else {
let console_type = match header[7] & 0b11 {
0 => ConsoleType::NESFamicom,
1 => ConsoleType::VsSystem,
_ => ConsoleType::Playchoice10,
};
// Either PRG-RAM or PRG-NVRAM, depending on the battery flag in byte 6
let mut some_prg_ram_bytes = 0x2000;
// "A general rule of thumb: if the last 4 bytes are not all zero, and the header is not marked for NES 2.0 format,
// an emulator should either mask off the upper 4 bits of the mapper number or simply refuse to load the ROM."
if header[12] == 0 && header[13] == 0 && header[14] == 0 && header[15] == 0 {
mapper_num |= (header[7] & 0xF0) as u16;
// "Size of PRG RAM in 8 KB units (Value 0 infers 8 KB for compatibility; see PRG RAM circuit)"
some_prg_ram_bytes *= std::cmp::max(header[8], 1) as usize;
}
let chr_ram_bytes = if n_chr_banks == 0 { 0x2000 } else { 0 };
let (prg_ram_bytes, prg_nvram_bytes) = if has_battery {
(0, some_prg_ram_bytes)
} else {
(some_prg_ram_bytes, 0)
};
(
console_type,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
0,
)
};
Ok(Self {
is_nes2,
n_prg_banks,
n_chr_banks,
hardwired_mirroring,
@ -83,13 +151,12 @@ impl CartridgeMetadata {
mapper_num,
submapper_num,
console_type,
prg_ram_shifts,
prg_nvram_shifts,
chr_ram_shifts,
chr_nvram_shifts,
prg_ram_bytes,
prg_nvram_bytes,
chr_ram_bytes,
chr_nvram_bytes,
timing,
vs_system_type,
extended_console_type,
n_misc_roms,
default_expansion_device,
})
@ -109,7 +176,16 @@ pub enum ConsoleType {
NESFamicom,
VsSystem,
Playchoice10,
Extended,
DecimalFamiclone,
VT01Monochrome,
VT01STN,
VT02,
VT03,
VT09,
VT32,
VT369,
UM6578,
Other,
}
pub enum ClockTiming {

View file

@ -35,8 +35,10 @@ impl Memory for Mapper0 {
fn peek(&self, addr: u16) -> u8 {
if addr <= 0x1FFF {
self.chr_mem[addr as usize % self.chr_mem.len()]
} else {
} else if addr >= 0x8000 {
self.prg_rom[((addr as usize - 0x8000) % (self.n_prg_banks as usize * 0x4000))]
} else {
0
}
}
@ -46,7 +48,7 @@ impl Memory for Mapper0 {
let len = self.chr_mem.len();
self.chr_mem[addr as usize % len] = data;
}
} else {
} else if addr >= 0x8000 {
self.prg_rom[((addr as usize - 0x8000) % (self.n_prg_banks as usize * 0x4000))] = data;
}
}

View file

@ -39,7 +39,7 @@ impl Mapper1 {
control_register: 0b01100, // "MMC1 seems to reliably power on in the last bank"
chr_bank_0: 0,
chr_bank_1: 0,
prg_bank: 0,
prg_bank: 0b10000,
}
}
}
@ -84,11 +84,7 @@ impl Memory for Mapper1 {
fn peek(&self, addr: u16) -> u8 {
if 0x6000 <= addr && addr <= 0x7FFF {
return if self.prg_bank >> 4 == 0 {
self.prg_ram[(addr as usize) - 0x6000]
} else {
0
};
return self.prg_ram[(addr as usize) - 0x6000];
}
if addr <= 0x0FFF {
@ -172,6 +168,10 @@ impl Memory for Mapper1 {
return;
}
if addr < 0x8000 {
return;
}
if self.last_write_timer > 0 {
return;
}
@ -196,7 +196,7 @@ impl Memory for Mapper1 {
} else if 0xC000 <= addr && addr <= 0xDFFF {
self.chr_bank_1 = self.shift_register;
} else {
self.prg_bank = self.shift_register;
self.prg_bank = (self.shift_register) % (self.n_prg_banks as u8);
}
self.shift_register = 0b10000;
self.shift_write_count = 0;

View file

@ -31,7 +31,8 @@ impl Memory for Mapper3 {
if addr <= 0x1FFF {
self.chr_rom[(self.chr_bank as usize * 0x2000) + addr as usize]
} else if 0x8000 <= addr {
self.prg_rom[addr as usize - 0x8000]
let len = self.prg_rom.len();
self.prg_rom[(addr as usize - 0x8000) % len]
} else {
0x0
}

View file

@ -116,7 +116,7 @@ impl Mapper4 {
[r6, r7, second_last, last]
};
0x2000 * banks[(addr as usize - 0x8000) / 0x2000] as usize
0x2000 * (banks[(addr as usize - 0x8000) / 0x2000] % (self.n_prg_banks * 2)) as usize
}
}

View file

@ -0,0 +1,67 @@
use crate::cartridge::Mapper;
use crate::cartridge::Mirroring;
use memory::Memory;
// https://wiki.nesdev.com/w/index.php/INES_Mapper_071
pub struct Mapper71 {
n_prg_banks: u16,
prg_rom: Vec<u8>,
chr_mem: Vec<u8>,
prg_bank: u8,
mirroring_option: Option<Mirroring>,
}
impl Mapper for Mapper71 {
fn get_nametable_mirroring(&self) -> Option<Mirroring> {
self.mirroring_option
}
}
impl Mapper71 {
pub fn new(n_prg_banks: u16, prg_data: Vec<u8>) -> Self {
Self {
n_prg_banks,
chr_mem: vec![0; 0x2000],
prg_rom: prg_data,
prg_bank: 0,
mirroring_option: None,
}
}
}
impl Memory for Mapper71 {
fn read(&mut self, addr: u16) -> u8 {
self.peek(addr)
}
fn peek(&self, addr: u16) -> u8 {
if addr <= 0x1FFF {
self.chr_mem[addr as usize % self.chr_mem.len()]
} else if 0x8000 <= addr && addr <= 0xBFFF {
self.prg_rom[self.prg_bank as usize * 0x4000 + (addr as usize - 0x8000)]
} else if 0xC000 <= addr {
self.prg_rom[(self.n_prg_banks as usize - 1) * 0x4000 + (addr as usize - 0xC000)]
} else {
0x0
}
}
fn write(&mut self, addr: u16, data: u8) {
if addr <= 0x1FFF {
let len = self.chr_mem.len();
self.chr_mem[addr as usize % len] = data;
} else if 0x9000 <= addr && addr <= 0x9FFF {
// "For compatibility without using a submapper, FCEUX begins all games with fixed mirroring,
// and applies single screen mirroring only once $9000-9FFF is written, ignoring writes to $8000-8FFF."
self.mirroring_option = Some(if (data >> 4) & 1 == 1 {
Mirroring::SingleScreenUpper
} else {
Mirroring::SingleScreenLower
});
} else if addr >= 0xC000 {
self.prg_bank = data % self.n_prg_banks as u8;
}
}
}

View file

@ -7,6 +7,7 @@ mod mapper2;
mod mapper3;
mod mapper4;
mod mapper7;
mod mapper71;
mod mapper9;
pub use self::mapper0::Mapper0;
@ -15,8 +16,10 @@ pub use self::mapper2::Mapper2;
pub use self::mapper3::Mapper3;
pub use self::mapper4::Mapper4;
pub use self::mapper7::Mapper7;
pub use self::mapper71::Mapper71;
pub use self::mapper9::Mapper9;
// TODO: Add reset to more mappers so NES::reset works
pub trait Mapper: Memory {
fn get_nametable_mirroring(&self) -> Option<Mirroring> {
None // Unless otherwise specified, mirroring is hard-wired

View file

@ -52,10 +52,11 @@ impl Cartridge {
n_chr_banks,
prg_data,
chr_data,
meta.submapper_num == 1,
meta.submapper_num == Some(1),
)),
7 => Box::from(Mapper7::new(n_prg_banks, prg_data)),
9 => Box::from(Mapper9::new(n_prg_banks, prg_data, chr_data)),
71 => Box::from(Mapper71::new(n_prg_banks, prg_data)),
_ => return Err("unsupported mapper"),
};

View file

@ -27,6 +27,8 @@ pub struct NES {
cart: Rc<RefCell<Cartridge>>,
joy1: Rc<RefCell<dyn Controller>>,
joy2: Rc<RefCell<dyn Controller>>,
pub paused: bool,
}
impl NES {
@ -74,9 +76,18 @@ impl NES {
cart,
joy1,
joy2,
paused: false,
}
}
pub fn reset(&mut self) {
// TODO: It seems that something isn't resetting, causing some (mainly test) roms to run extremely slowly
self.cpu.borrow_mut().reset();
self.ppu.borrow_mut().reset();
self.apu.borrow_mut().reset();
self.cart.borrow_mut().reset();
}
/// Load a ROM from a file and reset the system, returning whether it succeeded
pub fn load_rom(&mut self, file: File) -> Result<(), &'static str> {
match Cartridge::from_file(file) {
@ -96,6 +107,10 @@ impl NES {
}
pub fn tick(&mut self) {
if self.paused {
return;
}
self.ppu.borrow_mut().frame_ready = false;
if let Some(log) = self.cpu.borrow_mut().tick() {
println!("{}", log);
@ -112,7 +127,7 @@ impl NES {
self.cart.borrow_mut().cycle(); // TODO: Probably per-ppu tick for some mappers
if self.ppu.borrow().nmi {
self.ppu.borrow_mut().nmi = false;
self.cpu.borrow_mut().nmi();
self.cpu.borrow_mut().nmi_timer = 2;
} else if self.cart.borrow_mut().check_irq() || self.apu.borrow_mut().check_irq() {
self.cpu.borrow_mut().irq();
}

View file

@ -10,11 +10,12 @@ pub struct NametableMemory {
memory: RAM,
}
// TODO: I think FourScreen should generally map in part or entirely to cartridge memory
impl NametableMemory {
pub fn new(cart: Rc<RefCell<Cartridge>>) -> Self {
Self {
cart,
memory: RAM::new(0x400 * 4, 0x0000),
memory: RAM::new(0x400 * 8, 0x0000),
}
}
@ -24,19 +25,15 @@ impl NametableMemory {
if _addr >= 0x3000 {
_addr -= 0x1000;
}
let mut fix_4s = 0;
let nt_mirroring = match self.cart.borrow().get_nametable_mirroring() {
Mirroring::Vertical => [0, 1, 0, 1],
Mirroring::Horizontal => [0, 0, 1, 1],
Mirroring::FourScreen => {
fix_4s = 0x2000;
[0, 1, 2, 3]
}
Mirroring::FourScreen => [0, 1, 2, 3],
Mirroring::SingleScreenUpper => [1, 1, 1, 1],
Mirroring::SingleScreenLower => [0, 0, 0, 0],
};
nt_mirroring[((_addr - 0x2000) / 0x400) as usize] * 0x400 + (_addr % 0x400) + fix_4s
nt_mirroring[((_addr - 0x2000) / 0x400) as usize] * 0x400 + (_addr % 0x400)
}
}

View file

@ -6,6 +6,7 @@ use std::thread;
use std::time;
use sdl2::audio::AudioSpecDesired;
use sdl2::controller::{Axis, Button};
use sdl2::event::Event as SDL_Event;
use sdl2::keyboard::Scancode;
use sdl2::pixels::PixelFormatEnum;
@ -24,6 +25,12 @@ const COLORS: &'static [i32] = &[
0x000000,
];
const SAMPLE_RATE: usize = 96000;
const DESIRED_AUDIO_DELAY_MS: usize = 60;
const DELAY_SAMPLES: usize =
(SAMPLE_RATE as f32 * (DESIRED_AUDIO_DELAY_MS as f32 / 1000.)) as usize;
const AUDIO_BUFF_THRESHOLD: usize = std::mem::size_of::<f32>() * DELAY_SAMPLES;
pub struct SDLUI {
sdl_context: Sdl,
canvas: WindowCanvas,
@ -40,7 +47,20 @@ impl SDLUI {
}
pub fn render_loop(&mut self) {
let game_controller_subsystem = self.sdl_context.game_controller().unwrap();
let available = game_controller_subsystem
.num_joysticks()
.map_err(|e| format!("can't enumerate joysticks: {}", e))
.unwrap();
let controller_opt = (0..available).find_map(|id| {
if !game_controller_subsystem.is_game_controller(id) {
return None;
}
game_controller_subsystem.open(id).ok()
});
self.canvas.set_scale(3.0, 3.0).unwrap();
self.canvas.set_blend_mode(sdl2::render::BlendMode::Blend);
let mut event_pump = self.sdl_context.event_pump().unwrap();
let creator = self.canvas.texture_creator();
@ -52,9 +72,9 @@ impl SDLUI {
let audio_subsystem = self.sdl_context.audio().unwrap();
let desired_spec = AudioSpecDesired {
freq: Some(96000),
freq: Some(SAMPLE_RATE as i32),
channels: Some(1), // mono
samples: None, // default sample size
samples: Some(1024),
};
let device = audio_subsystem
@ -83,31 +103,88 @@ impl SDLUI {
let cycles_per_interrupt = 50_000;
let mut fps_timer = time::Instant::now();
loop {
let mut audio_buff = Vec::new();
audio_buff.reserve(AUDIO_BUFF_THRESHOLD);
'main_loop: loop {
if !self.nes.borrow().has_cartridge() {
for event in event_pump.poll_iter() {
match event {
SDL_Event::Quit { .. } => std::process::exit(0),
SDL_Event::Quit { .. } => break 'main_loop,
_ => {}
}
}
continue;
}
if self.nes.borrow().paused {
for event in event_pump.poll_iter() {
match event {
SDL_Event::Quit { .. } => break 'main_loop,
_ => {}
}
}
// Draw the game screen below a gray tint and a pause icon (two parallel lines)
self.canvas.copy(&texture, None, None).unwrap();
self.canvas
.set_draw_color(sdl2::pixels::Color::RGBA(50, 50, 50, 215));
let canvas_size = self.canvas.output_size().unwrap();
self.canvas
.fill_rect(sdl2::rect::Rect::new(0, 0, canvas_size.0, canvas_size.1))
.unwrap();
self.canvas
.set_draw_color(sdl2::pixels::Color::RGB(225, 25, 25));
self.canvas
.fill_rect(sdl2::rect::Rect::new(
((canvas_size.0 / 6) - 13) as i32,
((canvas_size.1 / 6) - 15) as i32,
10,
30,
))
.unwrap();
self.canvas
.fill_rect(sdl2::rect::Rect::new(
((canvas_size.0 / 6) + 3) as i32,
((canvas_size.1 / 6) - 15) as i32,
10,
30,
))
.unwrap();
self.canvas.present();
continue;
}
self.nes.borrow_mut().tick();
if self.nes.borrow().get_shift_strobe() || cycle_interrupt_timer == 0 {
for event in event_pump.poll_iter() {
match event {
SDL_Event::Quit { .. } => std::process::exit(0),
SDL_Event::Quit { .. } => break 'main_loop,
_ => {}
}
}
const DEAD_ZONE: i16 = 10_000;
let joy_input = if let Some(controller) = &controller_opt {
let joy_x = controller.axis(Axis::LeftX);
let joy_y = controller.axis(Axis::LeftY);
vec![
joy_x > DEAD_ZONE || controller.button(Button::DPadRight), // Right
joy_x < -DEAD_ZONE || controller.button(Button::DPadLeft), // Left
joy_y > DEAD_ZONE || controller.button(Button::DPadDown), // Down
joy_y < -DEAD_ZONE || controller.button(Button::DPadUp), // Up
controller.button(Button::Start), // Start
controller.button(Button::Back), // Select
controller.button(Button::B), // B
controller.button(Button::A), // A
]
} else {
vec![false; 8]
};
let mut controller_byte = 0;
let kb_state = event_pump.keyboard_state();
for scancode in &controls {
let bit = kb_state.is_scancode_pressed(*scancode) as u8;
for i in 0..controls.len() {
let bit = (kb_state.is_scancode_pressed(controls[i]) || joy_input[i]) as u8;
controller_byte <<= 1;
controller_byte |= bit;
}
@ -116,9 +193,22 @@ impl SDLUI {
.try_fill_controller_shift(controller_byte);
}
// Basic dynamic sampling idea based on github.com/ltriant/nes:
// Keep the audio device fed with about DESIRED_AUDIO_DELAY_MS of samples,
// ceasing sampling while the device is above that threshold.
let mut buff = self.nes.borrow_mut().take_audio_buff();
if device.size() < AUDIO_BUFF_THRESHOLD as u32 {
// Simple volume attenuation, since there's no audio mixing implementation yet
for entry in buff.iter_mut() {
*entry *= 0.25;
}
audio_buff.append(&mut buff);
}
let framebuffer_option = self.nes.borrow().get_new_frame();
if let Some(framebuffer) = framebuffer_option {
device.queue(&self.nes.borrow_mut().take_audio_buff());
device.queue(&audio_buff);
audio_buff.clear();
if (frame_count + 1) % frames_per_rate_check == 0 {
if (frame_count + 1) % (frames_per_rate_check * checks_per_rate_report) == 0 {
self.canvas
@ -133,30 +223,21 @@ impl SDLUI {
frame_count += 1;
let mut pixel_i = 0;
let mut update = false;
for y in 0..240 {
for x in 0..256 {
let color = framebuffer[y][x];
let c = COLORS[(color as usize) % 64];
let (r, g, b) =
((c >> 16) as u8, ((c >> 8) & 0xFF) as u8, (c & 0xFF) as u8);
if screen_buff[pixel_i + 0] != r
|| screen_buff[pixel_i + 1] != g
|| screen_buff[pixel_i + 2] != b
{
screen_buff[pixel_i + 0] = r;
screen_buff[pixel_i + 1] = g;
screen_buff[pixel_i + 2] = b;
update = true;
}
screen_buff[pixel_i + 0] = r;
screen_buff[pixel_i + 1] = g;
screen_buff[pixel_i + 2] = b;
pixel_i += 3;
}
}
if update {
texture.update(None, &screen_buff, 256 * 3).unwrap();
self.canvas.copy(&texture, None, None).unwrap();
self.canvas.present();
}
texture.update(None, &screen_buff, 256 * 3).unwrap();
self.canvas.copy(&texture, None, None).unwrap();
self.canvas.present();
let elapsed = fps_timer.elapsed();
if elapsed < time::Duration::from_millis(16) {

View file

@ -19,7 +19,7 @@ use nwd::NwgUi;
use nwg::NativeUi;
#[derive(Default, NwgUi)]
pub struct BasicApp {
pub struct GameWindow {
nes: Rc<RefCell<NES>>,
#[nwg_control(size: (256 * 3, 240 * 3), position: (300, 300), title: "KindNES", flags: "WINDOW|VISIBLE")]
@ -29,21 +29,35 @@ pub struct BasicApp {
file_menu: nwg::Menu,
#[nwg_control(text: "Open ROM", parent: file_menu)]
#[nwg_events( OnMenuItemSelected: [BasicApp::open_rom_dialog] )]
#[nwg_events( OnMenuItemSelected: [GameWindow::open_rom_dialog] )]
open_item: nwg::MenuItem,
#[nwg_control(parent: file_menu)]
exit_separator: nwg::MenuSeparator,
#[nwg_control(text: "Exit", parent: file_menu)]
#[nwg_events( OnMenuItemSelected: [BasicApp::exit] )]
#[nwg_events( OnMenuItemSelected: [GameWindow::exit] )]
exit_item: nwg::MenuItem,
// TODO: Disable the children of this (but not the top-level) when no game is inserted
#[nwg_control(text: "Game")]
game_menu: nwg::Menu,
// TODO: Change the button to resume when it's paused
// TODO: Probably also bind it to escape... should that be build into the SDL ui?
#[nwg_control(text: "Pause", parent: game_menu)]
#[nwg_events( OnMenuItemSelected: [GameWindow::pause] )]
pause_item: nwg::MenuItem,
#[nwg_control(text: "Reset", parent: game_menu)]
#[nwg_events( OnMenuItemSelected: [GameWindow::reset] )]
reset_item: nwg::MenuItem,
#[nwg_resource(action: FileDialogAction::Open, title: "Open a .NES file")]
file_dialog: nwg::FileDialog,
}
impl BasicApp {
impl GameWindow {
fn open_rom_dialog(&self) {
if self.file_dialog.run(Some(&self.window)) {
if let Ok(item) = self.file_dialog.get_selected_item() {
@ -74,6 +88,16 @@ impl BasicApp {
nwg::stop_thread_dispatch();
std::process::exit(0);
}
fn reset(&self) {
self.nes.borrow_mut().reset();
}
fn pause(&self) {
let paused = self.nes.borrow().paused;
self.nes.borrow_mut().paused = !paused;
self.pause_item.set_checked(!paused);
}
}
fn main() {
@ -82,7 +106,7 @@ fn main() {
nwg::init().expect("Failed to init Native Windows GUI");
nwg::Font::set_global_family("Segoe UI").expect("Failed to set default font");
let app = BasicApp::build_ui(Default::default()).expect("Failed to build UI");
let app = GameWindow::build_ui(Default::default()).expect("Failed to build UI");
let [total_width, total_height] = [nwg::Monitor::width(), nwg::Monitor::height()];
let (width, height) = (256 * 3, 240 * 3);