mirror of
https://github.com/henryksloan/kind-nes.git
synced 2025-04-02 10:31:47 -04:00
Compare commits
16 commits
v0.8.0-bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
cb1742e26f | ||
|
259c3265fa | ||
|
0228c3a94a | ||
|
09cf4c2628 | ||
|
d8967f216c | ||
|
f23628a638 | ||
|
1d792bf9af | ||
|
af3acf8c85 | ||
|
54a628bbf9 | ||
|
27cadf6f5f | ||
|
6512bf1315 | ||
|
627dd2d70f | ||
|
14a4135f5b | ||
|
5eff260ac1 | ||
|
63dbaa1719 | ||
|
bcf67d12c8 |
17 changed files with 437 additions and 120 deletions
23
README.md
23
README.md
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
67
nes/src/cartridge/mapper/mapper71.rs
Normal file
67
nes/src/cartridge/mapper/mapper71.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"),
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Reference in a new issue