mirror of
https://github.com/FranLMSP/rmg-001.git
synced 2024-11-27 03:31:31 +00:00
Compare commits
9 Commits
f90900a4c1
...
af15c0ef68
Author | SHA1 | Date | |
---|---|---|---|
af15c0ef68 | |||
292c7d1507 | |||
b5bb582c54 | |||
b0c07a5264 | |||
5723c3b3b6 | |||
61db367f31 | |||
abbd46ebed | |||
66731e1c8e | |||
c77bc9db70 |
@ -7,8 +7,9 @@ Any help or suggestion is welcome!
|
|||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- [x] CPU implementation
|
- [x] CPU implementation
|
||||||
- [ ] Interrupts
|
- [x] Interrupts (interrupts test not passing yet)
|
||||||
- [ ] Timing
|
- [x] Timer
|
||||||
|
- [x] Joypad (not configurable yet)
|
||||||
- [ ] PPU implementations
|
- [ ] PPU implementations
|
||||||
- [ ] Gameboy boot ROM
|
- [ ] Gameboy boot ROM
|
||||||
- [x] Render the pixels
|
- [x] Render the pixels
|
||||||
|
66
src/bus.rs
66
src/bus.rs
@ -5,8 +5,10 @@ use crate::utils::{
|
|||||||
join_bytes
|
join_bytes
|
||||||
};
|
};
|
||||||
use crate::rom::ROM;
|
use crate::rom::ROM;
|
||||||
use crate::ppu::{PPU, LCDStatus, LCDStatusModeFlag};
|
use crate::ppu::{PPU, LCDStatus, LCDStatusModeFlag, LCD_STATUS_ADDRESS, LCD_CONTROL_ADDRESS, LCD_Y_ADDRESS};
|
||||||
use crate::cpu::{InterruptFlag};
|
use crate::cpu::{Interrupt};
|
||||||
|
use crate::timer::{TIMER_DIVIDER_REGISTER_ADDRESS};
|
||||||
|
use crate::joypad::{Joypad, JOYPAD_ADDRESS};
|
||||||
|
|
||||||
pub struct AddressRange {
|
pub struct AddressRange {
|
||||||
begin: u16,
|
begin: u16,
|
||||||
@ -39,6 +41,7 @@ pub const NOT_USABLE: AddressRange = AddressRange{begin: 0xFEA0,
|
|||||||
pub const IO_REGISTERS: AddressRange = AddressRange{begin: 0xFF00, end: 0xFF7F};
|
pub const IO_REGISTERS: AddressRange = AddressRange{begin: 0xFF00, end: 0xFF7F};
|
||||||
pub const HIGH_RAM: AddressRange = AddressRange{begin: 0xFF80, end: 0xFFFE};
|
pub const HIGH_RAM: AddressRange = AddressRange{begin: 0xFF80, end: 0xFFFE};
|
||||||
pub const INTERRUPT_ENABLE_REGISTER: AddressRange = AddressRange{begin: 0xFFFF, end: 0xFFFF};
|
pub const INTERRUPT_ENABLE_REGISTER: AddressRange = AddressRange{begin: 0xFFFF, end: 0xFFFF};
|
||||||
|
pub const INTERRUPT_ENABLE_ADDRESS: u16 = 0xFFFF;
|
||||||
pub const INTERRUPT_FLAG_ADDRESS: u16 = 0xFF0F;
|
pub const INTERRUPT_FLAG_ADDRESS: u16 = 0xFF0F;
|
||||||
|
|
||||||
pub struct Bus {
|
pub struct Bus {
|
||||||
@ -51,6 +54,7 @@ impl Bus {
|
|||||||
let game_rom = match ROM::load_file("ignore/tetris.gb".to_string()) {
|
let game_rom = match ROM::load_file("ignore/tetris.gb".to_string()) {
|
||||||
// let game_rom = match ROM::load_file("roms/cpu_instrs.gb".to_string()) {
|
// let game_rom = match ROM::load_file("roms/cpu_instrs.gb".to_string()) {
|
||||||
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/01-special.gb".to_string()) {
|
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/01-special.gb".to_string()) {
|
||||||
|
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/02-interrupts.gb".to_string()) {
|
||||||
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/03-op sp,hl.gb".to_string()) {
|
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/03-op sp,hl.gb".to_string()) {
|
||||||
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/04-op r,imm.gb".to_string()) {
|
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/04-op r,imm.gb".to_string()) {
|
||||||
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/05-op rp.gb".to_string()) {
|
// let game_rom = match ROM::load_file("roms/cpu_instrs_individual/05-op rp.gb".to_string()) {
|
||||||
@ -64,8 +68,10 @@ impl Bus {
|
|||||||
// _ => ROM::from_bytes(&[0; 0xFFFF])
|
// _ => ROM::from_bytes(&[0; 0xFFFF])
|
||||||
_ => panic!("Could not read ROM"),
|
_ => panic!("Could not read ROM"),
|
||||||
};
|
};
|
||||||
|
let mut data = [0x00; 0x10000];
|
||||||
|
data[JOYPAD_ADDRESS as usize] = 0b11001111;
|
||||||
Self {
|
Self {
|
||||||
data: [0x00; 0x10000],
|
data,
|
||||||
game_rom,
|
game_rom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,12 +79,9 @@ impl Bus {
|
|||||||
pub fn read(&self, address: u16) -> u8 {
|
pub fn read(&self, address: u16) -> u8 {
|
||||||
if BANK_ZERO.in_range(address) || BANK_SWITCHABLE.in_range(address) {
|
if BANK_ZERO.in_range(address) || BANK_SWITCHABLE.in_range(address) {
|
||||||
return self.game_rom.read(address);
|
return self.game_rom.read(address);
|
||||||
} else if VIDEO_RAM.in_range(address) {
|
}
|
||||||
if PPU::get_lcd_status(self, LCDStatus::ModeFlag(LCDStatusModeFlag::TransferringToLCD)) {
|
if address == JOYPAD_ADDRESS {
|
||||||
return 0xFF
|
println!("Joypad read {:08b}", self.data[address as usize]);
|
||||||
}
|
|
||||||
} else if IO_REGISTERS.in_range(address) {
|
|
||||||
return self.data[address as usize];
|
|
||||||
}
|
}
|
||||||
self.data[address as usize]
|
self.data[address as usize]
|
||||||
}
|
}
|
||||||
@ -89,11 +92,11 @@ impl Bus {
|
|||||||
|
|
||||||
pub fn write(&mut self, address: u16, data: u8) {
|
pub fn write(&mut self, address: u16, data: u8) {
|
||||||
if address == 0xFF01 {
|
if address == 0xFF01 {
|
||||||
print!("{}", data as char);
|
// print!("{}", data as char);
|
||||||
}
|
}
|
||||||
|
|
||||||
if BANK_ZERO.in_range(address) || BANK_SWITCHABLE.in_range(address) {
|
if BANK_ZERO.in_range(address) || BANK_SWITCHABLE.in_range(address) {
|
||||||
println!("WRITING TO ROM");
|
// println!("WRITING TO ROM");
|
||||||
} else if WORK_RAM_1.in_range(address) || WORK_RAM_2.in_range(address) {
|
} else if WORK_RAM_1.in_range(address) || WORK_RAM_2.in_range(address) {
|
||||||
self.data[address as usize] = data;
|
self.data[address as usize] = data;
|
||||||
// Copy to the ECHO RAM
|
// Copy to the ECHO RAM
|
||||||
@ -103,29 +106,46 @@ impl Bus {
|
|||||||
} else if ECHO_RAM.in_range(address) {
|
} else if ECHO_RAM.in_range(address) {
|
||||||
self.data[address as usize] = data;
|
self.data[address as usize] = data;
|
||||||
self.data[(WORK_RAM_1.begin() + (address - ECHO_RAM.begin())) as usize] = data; // Copy to the working RAM
|
self.data[(WORK_RAM_1.begin() + (address - ECHO_RAM.begin())) as usize] = data; // Copy to the working RAM
|
||||||
} else if VIDEO_RAM.in_range(address) {
|
} else if address == TIMER_DIVIDER_REGISTER_ADDRESS {
|
||||||
//if !PPU::get_lcd_status(self, LCDStatus::ModeFlag(LCDStatusModeFlag::TransferringToLCD)) {
|
self.data[address as usize] = 0x00;
|
||||||
self.data[address as usize] = data;
|
} else if address == LCD_STATUS_ADDRESS {
|
||||||
// }
|
// Prevent user from modifying LCD Status mode
|
||||||
|
let byte = self.data[address as usize];
|
||||||
|
self.data[address as usize] = (data & 0b1111_1100) | (byte & 0b0000_0011);
|
||||||
|
} else if address == LCD_CONTROL_ADDRESS && get_bit(data, BitIndex::I7) {
|
||||||
|
self.data[address as usize] = data;
|
||||||
|
self.data[LCD_Y_ADDRESS as usize] = 0x00;
|
||||||
|
} else if address == JOYPAD_ADDRESS {
|
||||||
|
println!("Joypad write: {:08b}", data);
|
||||||
|
let byte = self.data[JOYPAD_ADDRESS as usize];
|
||||||
|
self.data[JOYPAD_ADDRESS as usize] = (data & 0b00110000) | 0b11000000 | (byte & 0b00001111);
|
||||||
} else {
|
} else {
|
||||||
self.data[address as usize] = data;
|
self.data[address as usize] = data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn force_write(&mut self, address: u16, data: u8) {
|
||||||
|
self.data[address as usize] = data;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn write_16bit(&mut self, address: u16, data: u16) {
|
pub fn write_16bit(&mut self, address: u16, data: u16) {
|
||||||
let bytes = data.to_le_bytes();
|
let bytes = data.to_le_bytes();
|
||||||
self.write(address, bytes[0]);
|
self.write(address, bytes[0]);
|
||||||
self.write(address.wrapping_add(1), bytes[1]);
|
self.write(address.wrapping_add(1), bytes[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_flag(&mut self, flag: InterruptFlag, val: bool) {
|
pub fn set_interrupt_enable(&mut self, interrupt: Interrupt, val: bool) {
|
||||||
|
let byte = self.read(INTERRUPT_ENABLE_ADDRESS);
|
||||||
|
self.write(INTERRUPT_ENABLE_ADDRESS, interrupt.set(byte, val));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_interrupt_flag(&mut self, interrupt: Interrupt, val: bool) {
|
||||||
let byte = self.read(INTERRUPT_FLAG_ADDRESS);
|
let byte = self.read(INTERRUPT_FLAG_ADDRESS);
|
||||||
self.write(INTERRUPT_FLAG_ADDRESS, match flag {
|
self.write(INTERRUPT_FLAG_ADDRESS, interrupt.set(byte, val));
|
||||||
InterruptFlag::VBlank => set_bit(byte, val, BitIndex::I0),
|
}
|
||||||
InterruptFlag::LCDSTAT => set_bit(byte, val, BitIndex::I1),
|
|
||||||
InterruptFlag::Timer => set_bit(byte, val, BitIndex::I2),
|
pub fn get_interrupt(&mut self, interrupt: Interrupt) -> bool {
|
||||||
InterruptFlag::Serial => set_bit(byte, val, BitIndex::I3),
|
let byte = self.read(INTERRUPT_ENABLE_ADDRESS) & self.read(INTERRUPT_FLAG_ADDRESS);
|
||||||
InterruptFlag::Joypad => set_bit(byte, val, BitIndex::I4),
|
interrupt.get(byte)
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
123
src/cpu.rs
123
src/cpu.rs
@ -8,7 +8,7 @@ use crate::utils::{
|
|||||||
sub_half_carry,
|
sub_half_carry,
|
||||||
add_half_carry_16bit,
|
add_half_carry_16bit,
|
||||||
};
|
};
|
||||||
use crate::bus::Bus;
|
use crate::bus::{Bus, INTERRUPT_ENABLE_ADDRESS, INTERRUPT_FLAG_ADDRESS};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum Register {
|
pub enum Register {
|
||||||
@ -53,7 +53,8 @@ pub enum FlagRegister {
|
|||||||
Carry, // Set if a carry was ocurrend from the last math operation or if register A is the smaller value when executing the CP instruction
|
Carry, // Set if a carry was ocurrend from the last math operation or if register A is the smaller value when executing the CP instruction
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum InterruptFlag {
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum Interrupt {
|
||||||
VBlank,
|
VBlank,
|
||||||
LCDSTAT,
|
LCDSTAT,
|
||||||
Timer,
|
Timer,
|
||||||
@ -61,14 +62,32 @@ pub enum InterruptFlag {
|
|||||||
Joypad,
|
Joypad,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InterruptFlag {
|
impl Interrupt {
|
||||||
pub fn get_bit_index(interrupt: InterruptFlag) -> BitIndex {
|
fn get_bit_index(&self) -> BitIndex {
|
||||||
match interrupt {
|
match self {
|
||||||
InterruptFlag::VBlank => BitIndex::I0,
|
Interrupt::VBlank => BitIndex::I0,
|
||||||
InterruptFlag::LCDSTAT => BitIndex::I1,
|
Interrupt::LCDSTAT => BitIndex::I1,
|
||||||
InterruptFlag::Timer => BitIndex::I2,
|
Interrupt::Timer => BitIndex::I2,
|
||||||
InterruptFlag::Serial => BitIndex::I3,
|
Interrupt::Serial => BitIndex::I3,
|
||||||
InterruptFlag::Joypad => BitIndex::I4,
|
Interrupt::Joypad => BitIndex::I4,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, byte: u8) -> bool {
|
||||||
|
get_bit(byte, self.get_bit_index())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, byte: u8, val: bool) -> u8 {
|
||||||
|
set_bit(byte, val, self.get_bit_index())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vector(&self) -> u16 {
|
||||||
|
match self {
|
||||||
|
Interrupt::VBlank => 0x40,
|
||||||
|
Interrupt::LCDSTAT => 0x48,
|
||||||
|
Interrupt::Timer => 0x50,
|
||||||
|
Interrupt::Serial => 0x58,
|
||||||
|
Interrupt::Joypad => 0x60,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -811,11 +830,19 @@ pub enum Opcode {
|
|||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct Cycles(pub usize);
|
pub struct Cycles(pub usize);
|
||||||
|
|
||||||
|
impl Cycles {
|
||||||
|
pub fn to_t(&self) -> usize {
|
||||||
|
self.0 * 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct CPU {
|
pub struct CPU {
|
||||||
registers: Registers,
|
registers: Registers,
|
||||||
cycles: Cycles,
|
cycles: Cycles,
|
||||||
last_op_cycles: Cycles,
|
last_op_cycles: Cycles,
|
||||||
exec_calls_count: usize,
|
exec_calls_count: usize,
|
||||||
|
is_halted: bool,
|
||||||
|
ime: bool, // Interrupt Master Enable
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CPU {
|
impl CPU {
|
||||||
@ -825,6 +852,8 @@ impl CPU {
|
|||||||
cycles: Cycles(0),
|
cycles: Cycles(0),
|
||||||
last_op_cycles: Cycles(0),
|
last_op_cycles: Cycles(0),
|
||||||
exec_calls_count: 0,
|
exec_calls_count: 0,
|
||||||
|
is_halted: false,
|
||||||
|
ime: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -837,8 +866,11 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn increment_cycles(&mut self, cycles: Cycles) {
|
fn increment_cycles(&mut self, cycles: Cycles) {
|
||||||
let Cycles(c) = cycles;
|
self.cycles.0 += cycles.0;
|
||||||
self.cycles.0 += c;
|
}
|
||||||
|
|
||||||
|
fn decrement_cycles(&mut self, cycles: Cycles) {
|
||||||
|
self.cycles.0 -= cycles.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset_cycles(&mut self) {
|
pub fn reset_cycles(&mut self) {
|
||||||
@ -876,20 +908,54 @@ impl CPU {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_interrupt(&mut self, bus: &mut Bus, interrupt: Interrupt) {
|
||||||
|
bus.set_interrupt_enable(interrupt, false);
|
||||||
|
bus.set_interrupt_flag(interrupt, false);
|
||||||
|
let vector = interrupt.get_vector();
|
||||||
|
self.exec(Opcode::CALL(OpcodeParameter::U16(vector)), bus);
|
||||||
|
println!("Interrupt: {:?}", interrupt);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_interrupts(&mut self, bus: &mut Bus) -> Option<Interrupt> {
|
||||||
|
if !self.ime && !self.is_halted {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if bus.read(INTERRUPT_ENABLE_ADDRESS) & bus.read(INTERRUPT_FLAG_ADDRESS) != 0 {
|
||||||
|
self.is_halted = false;
|
||||||
|
}
|
||||||
|
if bus.get_interrupt(Interrupt::VBlank) {
|
||||||
|
return Some(Interrupt::VBlank);
|
||||||
|
} else if bus.get_interrupt(Interrupt::LCDSTAT) {
|
||||||
|
return Some(Interrupt::LCDSTAT);
|
||||||
|
} else if bus.get_interrupt(Interrupt::Timer) {
|
||||||
|
return Some(Interrupt::Timer);
|
||||||
|
} else if bus.get_interrupt(Interrupt::Serial) {
|
||||||
|
return Some(Interrupt::Serial);
|
||||||
|
} else if bus.get_interrupt(Interrupt::Joypad) {
|
||||||
|
return Some(Interrupt::Joypad);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
pub fn run(&mut self, bus: &mut Bus) {
|
pub fn run(&mut self, bus: &mut Bus) {
|
||||||
let cycles_start = self.get_cycles();
|
let cycles_start = self.get_cycles();
|
||||||
let program_counter = self.registers.get(Register::PC);
|
if let Some(interrupt) = self.check_interrupts(bus) {
|
||||||
let parameter_bytes = OpcodeParameterBytes::from_address(program_counter, bus);
|
self.handle_interrupt(bus, interrupt);
|
||||||
let (opcode, cycles) = parameter_bytes.parse_opcode();
|
self.increment_cycles(Cycles(5));
|
||||||
if !env::var("CPU_LOG").is_err() {
|
} else if !self.is_halted {
|
||||||
self.log(parameter_bytes);
|
let program_counter = self.registers.get(Register::PC);
|
||||||
|
let parameter_bytes = OpcodeParameterBytes::from_address(program_counter, bus);
|
||||||
|
let (opcode, cycles) = parameter_bytes.parse_opcode();
|
||||||
|
if !env::var("CPU_LOG").is_err() {
|
||||||
|
self.log(parameter_bytes);
|
||||||
|
}
|
||||||
|
self.increment_cycles(cycles);
|
||||||
|
self.exec(opcode, bus);
|
||||||
|
} else if self.is_halted {
|
||||||
|
self.increment_cycles(Cycles(1));
|
||||||
}
|
}
|
||||||
self.increment_cycles(cycles);
|
|
||||||
self.exec(opcode, bus);
|
|
||||||
let cycles_end = self.get_cycles();
|
let cycles_end = self.get_cycles();
|
||||||
|
|
||||||
self.set_last_op_cycles(cycles_start, cycles_end);
|
self.set_last_op_cycles(cycles_start, cycles_end);
|
||||||
// self.increment_exec_calls_count();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(&mut self, opcode: Opcode, bus: &mut Bus) {
|
pub fn exec(&mut self, opcode: Opcode, bus: &mut Bus) {
|
||||||
@ -1713,21 +1779,22 @@ impl CPU {
|
|||||||
// Enable interrupts
|
// Enable interrupts
|
||||||
Opcode::EI => {
|
Opcode::EI => {
|
||||||
self.registers.increment(Register::PC, 1);
|
self.registers.increment(Register::PC, 1);
|
||||||
bus.write(0xFFFF, 0xFF); // Disable all interrupts
|
self.ime = true;
|
||||||
},
|
},
|
||||||
// Disable interrupts
|
// Disable interrupts
|
||||||
Opcode::DI => {
|
Opcode::DI => {
|
||||||
self.registers.increment(Register::PC, 1);
|
self.registers.increment(Register::PC, 1);
|
||||||
bus.write(0xFFFF, 0x00); // Disable all interrupts
|
self.ime = false;
|
||||||
},
|
},
|
||||||
// Same as enabling interrupts and then executing RET
|
// Same as enabling interrupts and then executing RET
|
||||||
Opcode::RETI => {
|
Opcode::RETI => {
|
||||||
self.exec(Opcode::EI, bus);
|
self.exec(Opcode::EI, bus);
|
||||||
self.exec(Opcode::RET(OpcodeParameter::NoParam), bus);
|
self.exec(Opcode::RET(OpcodeParameter::NoParam), bus);
|
||||||
},
|
},
|
||||||
// WIP
|
// Don't execute instructions until an interrupt is requested
|
||||||
Opcode::HALT => {
|
Opcode::HALT => {
|
||||||
self.registers.increment(Register::PC, 1);
|
self.registers.increment(Register::PC, 1);
|
||||||
|
self.is_halted = true;
|
||||||
},
|
},
|
||||||
Opcode::STOP => {
|
Opcode::STOP => {
|
||||||
self.registers.increment(Register::PC, 2);
|
self.registers.increment(Register::PC, 2);
|
||||||
@ -1865,16 +1932,16 @@ mod tests {
|
|||||||
let mut bus = Bus::new();
|
let mut bus = Bus::new();
|
||||||
let addr = 0xFF00;
|
let addr = 0xFF00;
|
||||||
cpu.registers.set(Register::A, 0xF1);
|
cpu.registers.set(Register::A, 0xF1);
|
||||||
cpu.exec(Opcode::LD(OpcodeParameter::FF00plusU8_Register(4, Register::A)), &mut bus);
|
cpu.exec(Opcode::LD(OpcodeParameter::FF00plusU8_Register(0x42, Register::A)), &mut bus);
|
||||||
assert_eq!(bus.read(addr + 4), 0xF1);
|
assert_eq!(bus.read(addr + 0x42), 0xF1);
|
||||||
assert_eq!(cpu.registers.get(Register::PC), 0x102);
|
assert_eq!(cpu.registers.get(Register::PC), 0x102);
|
||||||
|
|
||||||
let mut cpu = CPU::new();
|
let mut cpu = CPU::new();
|
||||||
let mut bus = Bus::new();
|
let mut bus = Bus::new();
|
||||||
let addr = 0xFF00;
|
let addr = 0xFF00;
|
||||||
cpu.registers.set(Register::A, 0x00);
|
cpu.registers.set(Register::A, 0x00);
|
||||||
bus.write(addr + 4, 0xF1);
|
bus.write(addr + 0x42, 0xF1);
|
||||||
cpu.exec(Opcode::LD(OpcodeParameter::Register_FF00plusU8(Register::A, 4)), &mut bus);
|
cpu.exec(Opcode::LD(OpcodeParameter::Register_FF00plusU8(Register::A, 0x42)), &mut bus);
|
||||||
assert_eq!(cpu.registers.get(Register::A), 0xF1);
|
assert_eq!(cpu.registers.get(Register::A), 0xF1);
|
||||||
assert_eq!(cpu.registers.get(Register::PC), 0x102);
|
assert_eq!(cpu.registers.get(Register::PC), 0x102);
|
||||||
|
|
||||||
|
@ -1,26 +1,107 @@
|
|||||||
use std::{thread, time};
|
use std::{thread, time};
|
||||||
|
use winit_input_helper::WinitInputHelper;
|
||||||
|
use winit::event::{VirtualKeyCode};
|
||||||
|
|
||||||
use crate::cpu::{CPU, Cycles};
|
use crate::cpu::{CPU, Cycles, Interrupt};
|
||||||
use crate::ppu::PPU;
|
use crate::ppu::PPU;
|
||||||
use crate::bus::Bus;
|
use crate::bus::Bus;
|
||||||
|
use crate::timer::Timer;
|
||||||
|
use crate::joypad::{Joypad, Button, JOYPAD_ADDRESS};
|
||||||
|
|
||||||
pub struct Emulator {
|
pub struct Emulator {
|
||||||
cpu: CPU,
|
cpu: CPU,
|
||||||
ppu: PPU,
|
ppu: PPU,
|
||||||
bus: Bus,
|
bus: Bus,
|
||||||
|
timer: Timer,
|
||||||
|
joypad: Joypad,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Emulator {
|
impl Emulator {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let mut joypad: Joypad = Joypad::new();
|
||||||
Self {
|
Self {
|
||||||
cpu: CPU::new(),
|
cpu: CPU::new(),
|
||||||
ppu: PPU::new(),
|
ppu: PPU::new(),
|
||||||
bus: Bus::new(),
|
bus: Bus::new(),
|
||||||
|
timer: Timer::new(),
|
||||||
|
joypad: Joypad::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_input(&mut self, input: &WinitInputHelper) {
|
||||||
|
let mut change = false;
|
||||||
|
if input.key_pressed(VirtualKeyCode::K) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::A);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::J) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::B);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::W) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Up);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::S) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Down);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::A) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Left);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::D) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Right);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::N) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Start);
|
||||||
|
}
|
||||||
|
if input.key_pressed(VirtualKeyCode::B) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.press(Button::Select);
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.key_released(VirtualKeyCode::K) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::A);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::J) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::B);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::W) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Up);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::S) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Down);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::A) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Left);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::D) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Right);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::N) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Start);
|
||||||
|
}
|
||||||
|
if input.key_released(VirtualKeyCode::B) {
|
||||||
|
change = true;
|
||||||
|
self.joypad.release(Button::Select);
|
||||||
|
}
|
||||||
|
if change {
|
||||||
|
self.bus.force_write(JOYPAD_ADDRESS, self.joypad.get(&self.bus));
|
||||||
|
self.bus.set_interrupt_flag(Interrupt::Joypad, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw(&mut self, frame: &mut [u8]) {
|
pub fn draw(&mut self, frame: &mut [u8]) {
|
||||||
// self.ppu.draw_background(&mut self.bus);
|
|
||||||
let ppu_frame = self.ppu.get_rgba_frame();
|
let ppu_frame = self.ppu.get_rgba_frame();
|
||||||
for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
|
for (i, pixel) in frame.chunks_exact_mut(4).enumerate() {
|
||||||
pixel.copy_from_slice(&ppu_frame[i]);
|
pixel.copy_from_slice(&ppu_frame[i]);
|
||||||
@ -30,8 +111,9 @@ impl Emulator {
|
|||||||
pub fn run(&mut self, cpu_cycles: Cycles) {
|
pub fn run(&mut self, cpu_cycles: Cycles) {
|
||||||
self.cpu.reset_cycles();
|
self.cpu.reset_cycles();
|
||||||
while self.cpu.get_cycles().0 <= cpu_cycles.0 {
|
while self.cpu.get_cycles().0 <= cpu_cycles.0 {
|
||||||
self.ppu.do_cycle(&mut self.bus);
|
|
||||||
self.cpu.run(&mut self.bus);
|
self.cpu.run(&mut self.bus);
|
||||||
|
self.ppu.do_cycles(&mut self.bus, self.cpu.get_last_op_cycles());
|
||||||
|
self.timer.do_cycles(&mut self.bus, self.cpu.get_last_op_cycles());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +122,6 @@ impl Emulator {
|
|||||||
while !exit {
|
while !exit {
|
||||||
self.cpu.run(&mut self.bus);
|
self.cpu.run(&mut self.bus);
|
||||||
|
|
||||||
// thread::sleep(time::Duration::from_millis(100));
|
|
||||||
// exit = self.cpu.get_exec_calls_count() >= 1258895; // log 1
|
// exit = self.cpu.get_exec_calls_count() >= 1258895; // log 1
|
||||||
// exit = self.cpu.get_exec_calls_count() >= 1068422; // log 3
|
// exit = self.cpu.get_exec_calls_count() >= 1068422; // log 3
|
||||||
// exit = self.cpu.get_exec_calls_count() >= 1262766; // log 4
|
// exit = self.cpu.get_exec_calls_count() >= 1262766; // log 4
|
||||||
|
91
src/joypad.rs
Normal file
91
src/joypad.rs
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
use crate::bus::{Bus};
|
||||||
|
use crate::utils::{BitIndex, get_bit};
|
||||||
|
|
||||||
|
pub const JOYPAD_ADDRESS: u16 = 0xFF00;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub enum Button {
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
Up,
|
||||||
|
Down,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Start,
|
||||||
|
Select
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Joypad {
|
||||||
|
a: bool,
|
||||||
|
b: bool,
|
||||||
|
up: bool,
|
||||||
|
down: bool,
|
||||||
|
left: bool,
|
||||||
|
right: bool,
|
||||||
|
start: bool,
|
||||||
|
select: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Joypad {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
a: false,
|
||||||
|
b: false,
|
||||||
|
up: false,
|
||||||
|
down: false,
|
||||||
|
left: false,
|
||||||
|
right: false,
|
||||||
|
start: false,
|
||||||
|
select: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn press(&mut self, button: Button) {
|
||||||
|
println!("{:?} pressed", button);
|
||||||
|
match button {
|
||||||
|
Button::A => self.a = true,
|
||||||
|
Button::B => self.b = true,
|
||||||
|
Button::Up => self.up = true,
|
||||||
|
Button::Down => self.down = true,
|
||||||
|
Button::Left => self.left = true,
|
||||||
|
Button::Right => self.right = true,
|
||||||
|
Button::Start => self.start = true,
|
||||||
|
Button::Select => self.select = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release(&mut self, button: Button) {
|
||||||
|
println!("{:?} released", button);
|
||||||
|
match button {
|
||||||
|
Button::A => self.a = false,
|
||||||
|
Button::B => self.b = false,
|
||||||
|
Button::Up => self.up = false,
|
||||||
|
Button::Down => self.down = false,
|
||||||
|
Button::Left => self.left = false,
|
||||||
|
Button::Right => self.right = false,
|
||||||
|
Button::Start => self.start = false,
|
||||||
|
Button::Select => self.select = false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, bus: &Bus) -> u8 {
|
||||||
|
let byte = bus.read(JOYPAD_ADDRESS);
|
||||||
|
let direction = !get_bit(byte, BitIndex::I4);
|
||||||
|
let action = !get_bit(byte, BitIndex::I5);
|
||||||
|
|
||||||
|
let action = true;
|
||||||
|
let direction = true;
|
||||||
|
|
||||||
|
0b11000000 |
|
||||||
|
(byte & 0b00110000) |
|
||||||
|
(
|
||||||
|
(!((direction && self.down) || (action && self.start)) as u8) << 3
|
||||||
|
) | (
|
||||||
|
(!((direction && self.up) || (action && self.select)) as u8) << 2
|
||||||
|
) | (
|
||||||
|
(!((direction && self.left) || (action && self.b)) as u8) << 1
|
||||||
|
) | (
|
||||||
|
(!((direction && self.right) || (action && self.a)) as u8)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
pub mod utils;
|
pub mod utils;
|
||||||
pub mod cpu;
|
pub mod cpu;
|
||||||
pub mod ppu;
|
pub mod ppu;
|
||||||
|
pub mod timer;
|
||||||
pub mod rom;
|
pub mod rom;
|
||||||
pub mod bus;
|
pub mod bus;
|
||||||
|
pub mod joypad;
|
||||||
pub mod emulator;
|
pub mod emulator;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
281
src/ppu.rs
281
src/ppu.rs
@ -5,7 +5,7 @@ use crate::utils::{
|
|||||||
to_bit_index,
|
to_bit_index,
|
||||||
};
|
};
|
||||||
use crate::bus::{Bus, AddressRange, BANK_ZERO, VIDEO_RAM};
|
use crate::bus::{Bus, AddressRange, BANK_ZERO, VIDEO_RAM};
|
||||||
use crate::cpu::{Cycles, InterruptFlag};
|
use crate::cpu::{Cycles, Interrupt};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
enum Pixel {
|
enum Pixel {
|
||||||
@ -18,17 +18,41 @@ enum Pixel {
|
|||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
struct ColorPalette(u8, u8, u8, u8);
|
struct ColorPalette(u8, u8, u8, u8);
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub enum LCDControl {
|
pub enum LCDControl {
|
||||||
DisplayEnable,
|
LCDEnable,
|
||||||
WindowTileMapAddress,
|
WindowTileMapAddress,
|
||||||
WindowEnable,
|
WindowEnable,
|
||||||
BackgroundWindowTileAddress,
|
TileAddressMode,
|
||||||
BackgroundTileMapAddress,
|
BackgroundTileMapAddress,
|
||||||
ObjectSize,
|
ObjectSize,
|
||||||
ObjectEnable,
|
ObjectEnable,
|
||||||
BackgroundPriority,
|
BackgroundPriority,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl LCDControl {
|
||||||
|
fn get_bit_index(&self) -> BitIndex {
|
||||||
|
match self {
|
||||||
|
LCDControl::LCDEnable => BitIndex::I7,
|
||||||
|
LCDControl::WindowTileMapAddress => BitIndex::I6,
|
||||||
|
LCDControl::WindowEnable => BitIndex::I5,
|
||||||
|
LCDControl::TileAddressMode => BitIndex::I4,
|
||||||
|
LCDControl::BackgroundTileMapAddress => BitIndex::I3,
|
||||||
|
LCDControl::ObjectSize => BitIndex::I2,
|
||||||
|
LCDControl::ObjectEnable => BitIndex::I1,
|
||||||
|
LCDControl::BackgroundPriority => BitIndex::I0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, byte: u8) -> bool {
|
||||||
|
get_bit(byte, self.get_bit_index())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set(&self, byte: u8, val: bool) -> u8 {
|
||||||
|
set_bit(byte, val, self.get_bit_index())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub enum LCDStatusModeFlag {
|
pub enum LCDStatusModeFlag {
|
||||||
HBlank,
|
HBlank,
|
||||||
VBlank,
|
VBlank,
|
||||||
@ -51,26 +75,31 @@ pub const WIDTH: u32 = LCD_WIDTH;
|
|||||||
pub const HEIGHT: u32 = LCD_HEIGHT;
|
pub const HEIGHT: u32 = LCD_HEIGHT;
|
||||||
pub const FRAME_BUFFER_LENGTH: u32 = WIDTH * HEIGHT;
|
pub const FRAME_BUFFER_LENGTH: u32 = WIDTH * HEIGHT;
|
||||||
|
|
||||||
const LCD_CONTROL_ADDRESS: u16 = 0xFF40;
|
pub const LCD_CONTROL_ADDRESS: u16 = 0xFF40;
|
||||||
const LCD_STATUS_ADDRESS: u16 = 0xFF41;
|
pub const LCD_STATUS_ADDRESS: u16 = 0xFF41;
|
||||||
|
|
||||||
const SCROLL_Y_ADDRESS: u16 = 0xFF42;
|
pub const SCROLL_Y_ADDRESS: u16 = 0xFF42;
|
||||||
const SCROLL_X_ADDRESS: u16 = 0xFF43;
|
pub const SCROLL_X_ADDRESS: u16 = 0xFF43;
|
||||||
const LCD_Y_ADDRESS: u16 = 0xFF44;
|
pub const LCD_Y_ADDRESS: u16 = 0xFF44;
|
||||||
const LCD_Y_COMPARE_ADDRESS: u16 = 0xFF45;
|
pub const LCD_Y_COMPARE_ADDRESS: u16 = 0xFF45;
|
||||||
const DMA_ADDRESS: u16 = 0xFF46;
|
pub const DMA_ADDRESS: u16 = 0xFF46;
|
||||||
const BACKGROUND_PALETTE_ADDRESS: u16 = 0xFF47;
|
pub const BACKGROUND_PALETTE_ADDRESS: u16 = 0xFF47;
|
||||||
const OBJECT_PALETTE_0_ADDRESS: u16 = 0xFF48;
|
pub const OBJECT_PALETTE_0_ADDRESS: u16 = 0xFF48;
|
||||||
const OBJECT_PALETTE_1_ADDRESS: u16 = 0xFF49;
|
pub const OBJECT_PALETTE_1_ADDRESS: u16 = 0xFF49;
|
||||||
const WINDOW_X_ADDRESS: u16 = 0xFF4A;
|
pub const WINDOW_X_ADDRESS: u16 = 0xFF4A;
|
||||||
const WINDOW_Y_ADDRESS: u16 = 0xFF4B;
|
pub const WINDOW_Y_ADDRESS: u16 = 0xFF4B;
|
||||||
const TILE_MAP_ADDRESS: u16 = 0x9800;
|
pub const TILE_MAP_ADDRESS: u16 = 0x9800;
|
||||||
|
|
||||||
pub struct PPU {
|
pub struct PPU {
|
||||||
cycles: Cycles,
|
cycles: Cycles,
|
||||||
rgba_frame: [[u8; 4]; FRAME_BUFFER_LENGTH as usize],
|
rgba_frame: [[u8; 4]; FRAME_BUFFER_LENGTH as usize],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum TileNumber {
|
||||||
|
Base(u16),
|
||||||
|
Absolute(u8),
|
||||||
|
}
|
||||||
|
|
||||||
impl PPU {
|
impl PPU {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
@ -87,42 +116,57 @@ impl PPU {
|
|||||||
self.cycles.0 += cycles.0;
|
self.cycles.0 += cycles.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn do_cycle(&mut self, bus: &mut Bus) {
|
pub fn do_cycles(&mut self, bus: &mut Bus, cycles: Cycles) {
|
||||||
// Mode 1 Vertical blank
|
let mut count = 0;
|
||||||
if PPU::get_lcd_y(bus) >= 144 {
|
while count < cycles.0 {
|
||||||
if PPU::get_lcd_y(bus) == 144 {
|
self.cycle(bus);
|
||||||
bus.set_flag(InterruptFlag::VBlank, true);
|
count += 1;
|
||||||
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::VBlank), true);
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
pub fn cycle(&mut self, bus: &mut Bus) {
|
||||||
|
self.increment_cycles(Cycles(1));
|
||||||
|
if !PPU::get_lcd_control(bus, LCDControl::LCDEnable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if PPU::get_lcd_y(bus) < 144 {
|
||||||
if self.cycles.0 == 0 {
|
if self.cycles.0 == 0 {
|
||||||
// Mode 2 OAM scan
|
// Mode 2 OAM scan
|
||||||
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::SearchingOAM), true);
|
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::SearchingOAM), true);
|
||||||
|
if PPU::get_lcd_status(bus, LCDStatus::Mode2OAMInterrupt) {
|
||||||
|
PPU::request_interrupt(bus, Interrupt::LCDSTAT);
|
||||||
|
}
|
||||||
} else if self.cycles.0 == 80 + 1 {
|
} else if self.cycles.0 == 80 + 1 {
|
||||||
// Mode 3 drawing pixel line. This could also last 289 cycles
|
// Mode 3 drawing pixel line. This could also last 289 cycles
|
||||||
bus.set_flag(InterruptFlag::LCDSTAT, true);
|
|
||||||
self.draw_line(bus);
|
self.draw_line(bus);
|
||||||
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::TransferringToLCD), true);
|
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::TransferringToLCD), true);
|
||||||
} else if self.cycles.0 == 80 + 172 + 1 {
|
} else if self.cycles.0 == 80 + 172 + 1 {
|
||||||
// Mode 0 Horizontal blank. This could last 87 or 204 cycles depending on the mode 3
|
// Mode 0 Horizontal blank. This could last 87 or 204 cycles depending on the mode 3
|
||||||
bus.set_flag(InterruptFlag::LCDSTAT, true);
|
|
||||||
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::HBlank), true);
|
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::HBlank), true);
|
||||||
|
if PPU::get_lcd_status(bus, LCDStatus::Mode0HBlankInterrupt) {
|
||||||
|
PPU::request_interrupt(bus, Interrupt::LCDSTAT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if PPU::get_lcd_y(bus) == 144 && self.cycles.0 == 0 {
|
||||||
|
// Mode 1 Vertical blank
|
||||||
|
PPU::set_lcd_status(bus, LCDStatus::ModeFlag(LCDStatusModeFlag::VBlank), true);
|
||||||
|
if PPU::get_lcd_status(bus, LCDStatus::Mode1VBlankInterrupt) {
|
||||||
|
PPU::request_interrupt(bus, Interrupt::VBlank);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let lyc_compare = PPU::get_lcd_y(bus) == bus.read(LCD_Y_COMPARE_ADDRESS);
|
|
||||||
PPU::set_lcd_status(bus, LCDStatus::LYCInterrupt, lyc_compare);
|
|
||||||
if lyc_compare {
|
|
||||||
bus.set_flag(InterruptFlag::LCDSTAT, true);
|
|
||||||
}
|
|
||||||
self.increment_cycles(Cycles(1));
|
|
||||||
|
|
||||||
// Horizontal scan completed
|
// Horizontal scan completed
|
||||||
if self.cycles.0 > 456 {
|
if self.cycles.0 > 456 {
|
||||||
self.reset_cycles();
|
self.reset_cycles();
|
||||||
|
|
||||||
PPU::set_lcd_y(bus, PPU::get_lcd_y(bus) + 1);
|
PPU::set_lcd_y(bus, PPU::get_lcd_y(bus) + 1);
|
||||||
|
|
||||||
|
let lyc_compare = PPU::get_lcd_y(bus) == bus.read(LCD_Y_COMPARE_ADDRESS);
|
||||||
|
PPU::set_lcd_status(bus, LCDStatus::LYCFlag, lyc_compare);
|
||||||
|
if PPU::get_lcd_status(bus, LCDStatus::LYCInterrupt) && lyc_compare {
|
||||||
|
PPU::request_interrupt(bus, Interrupt::LCDSTAT);
|
||||||
|
}
|
||||||
// Frame completed
|
// Frame completed
|
||||||
if PPU::get_lcd_y(bus) > 153 {
|
if PPU::get_lcd_y(bus) > 153 {
|
||||||
PPU::set_lcd_y(bus, 0);
|
PPU::set_lcd_y(bus, 0);
|
||||||
@ -130,6 +174,12 @@ impl PPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn request_interrupt(bus: &mut Bus, interrupt: Interrupt) {
|
||||||
|
if PPU::get_lcd_control(bus, LCDControl::LCDEnable) {
|
||||||
|
bus.set_interrupt_flag(interrupt, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_lcd_y(bus: &Bus) -> u8 {
|
fn get_lcd_y(bus: &Bus) -> u8 {
|
||||||
bus.read(LCD_Y_ADDRESS)
|
bus.read(LCD_Y_ADDRESS)
|
||||||
}
|
}
|
||||||
@ -142,59 +192,40 @@ impl PPU {
|
|||||||
bus.read(SCROLL_X_ADDRESS)
|
bus.read(SCROLL_X_ADDRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_scroll_x(bus: &mut Bus, val: u8) {
|
|
||||||
bus.write(SCROLL_X_ADDRESS, val);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_scroll_y(bus: &Bus) -> u8 {
|
fn get_scroll_y(bus: &Bus) -> u8 {
|
||||||
bus.read(SCROLL_Y_ADDRESS)
|
bus.read(SCROLL_Y_ADDRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_scroll_y(bus: &mut Bus, val: u8) {
|
fn get_window_x(bus: &Bus) -> u8 {
|
||||||
bus.write(SCROLL_Y_ADDRESS, val);
|
bus.read(WINDOW_X_ADDRESS)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_window_y(bus: &Bus) -> u8 {
|
||||||
|
bus.read(WINDOW_Y_ADDRESS)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lcd_control(bus: &Bus, control: LCDControl) -> bool {
|
pub fn get_lcd_control(bus: &Bus, control: LCDControl) -> bool {
|
||||||
let byte = bus.read(LCD_CONTROL_ADDRESS);
|
let byte = bus.read(LCD_CONTROL_ADDRESS);
|
||||||
match control {
|
control.get(byte)
|
||||||
LCDControl::DisplayEnable => get_bit(byte, BitIndex::I7),
|
|
||||||
LCDControl::WindowTileMapAddress => get_bit(byte, BitIndex::I6),
|
|
||||||
LCDControl::WindowEnable => get_bit(byte, BitIndex::I5),
|
|
||||||
LCDControl::BackgroundWindowTileAddress => get_bit(byte, BitIndex::I4),
|
|
||||||
LCDControl::BackgroundTileMapAddress => get_bit(byte, BitIndex::I3),
|
|
||||||
LCDControl::ObjectSize => get_bit(byte, BitIndex::I2),
|
|
||||||
LCDControl::ObjectEnable => get_bit(byte, BitIndex::I1),
|
|
||||||
LCDControl::BackgroundPriority => get_bit(byte, BitIndex::I0),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_lcd_control(bus: &mut Bus, control: LCDControl, val: bool) {
|
fn set_lcd_control(bus: &mut Bus, control: LCDControl, val: bool) {
|
||||||
let mut byte = bus.read(LCD_CONTROL_ADDRESS);
|
let mut byte = bus.read(LCD_CONTROL_ADDRESS);
|
||||||
byte = match control {
|
bus.write(LCD_CONTROL_ADDRESS, control.set(byte, val));
|
||||||
LCDControl::DisplayEnable => set_bit(byte, val, BitIndex::I7),
|
|
||||||
LCDControl::WindowTileMapAddress => set_bit(byte, val, BitIndex::I6),
|
|
||||||
LCDControl::WindowEnable => set_bit(byte, val, BitIndex::I5),
|
|
||||||
LCDControl::BackgroundWindowTileAddress => set_bit(byte, val, BitIndex::I4),
|
|
||||||
LCDControl::BackgroundTileMapAddress => set_bit(byte, val, BitIndex::I3),
|
|
||||||
LCDControl::ObjectSize => set_bit(byte, val, BitIndex::I2),
|
|
||||||
LCDControl::ObjectEnable => set_bit(byte, val, BitIndex::I1),
|
|
||||||
LCDControl::BackgroundPriority => set_bit(byte, val, BitIndex::I0),
|
|
||||||
};
|
|
||||||
bus.write(LCD_CONTROL_ADDRESS, byte);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_lcd_status(bus: &Bus, status: LCDStatus) -> bool {
|
pub fn get_lcd_status(bus: &Bus, status: LCDStatus) -> bool {
|
||||||
let byte = bus.read(LCD_STATUS_ADDRESS);
|
let byte = bus.read(LCD_STATUS_ADDRESS);
|
||||||
match status {
|
match status {
|
||||||
LCDStatus::LYCInterrupt => get_bit(byte, BitIndex::I6),
|
LCDStatus::LYCInterrupt => get_bit(byte, BitIndex::I6),
|
||||||
LCDStatus::Mode2OAMInterrupt => get_bit(byte, BitIndex::I5),
|
LCDStatus::Mode2OAMInterrupt => get_bit(byte, BitIndex::I5),
|
||||||
LCDStatus::Mode1VBlankInterrupt => get_bit(byte, BitIndex::I4),
|
LCDStatus::Mode1VBlankInterrupt => get_bit(byte, BitIndex::I4),
|
||||||
LCDStatus::Mode0HBlankInterrupt => get_bit(byte, BitIndex::I3),
|
LCDStatus::Mode0HBlankInterrupt => get_bit(byte, BitIndex::I3),
|
||||||
LCDStatus::LYCFlag => get_bit(byte, BitIndex::I2),
|
LCDStatus::LYCFlag => get_bit(byte, BitIndex::I2),
|
||||||
LCDStatus::ModeFlag(mode) => match mode {
|
LCDStatus::ModeFlag(mode) => match mode {
|
||||||
LCDStatusModeFlag::HBlank => (byte & 0b00000011) == 0,
|
LCDStatusModeFlag::HBlank => (byte & 0b00000011) == 0,
|
||||||
LCDStatusModeFlag::VBlank => (byte & 0b00000011) == 1,
|
LCDStatusModeFlag::VBlank => (byte & 0b00000011) == 1,
|
||||||
LCDStatusModeFlag::SearchingOAM => (byte & 0b00000011) == 2,
|
LCDStatusModeFlag::SearchingOAM => (byte & 0b00000011) == 2,
|
||||||
LCDStatusModeFlag::TransferringToLCD => (byte & 0b00000011) == 3,
|
LCDStatusModeFlag::TransferringToLCD => (byte & 0b00000011) == 3,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -203,22 +234,70 @@ impl PPU {
|
|||||||
fn set_lcd_status(bus: &mut Bus, status: LCDStatus, val: bool) {
|
fn set_lcd_status(bus: &mut Bus, status: LCDStatus, val: bool) {
|
||||||
let mut byte = bus.read(LCD_STATUS_ADDRESS);
|
let mut byte = bus.read(LCD_STATUS_ADDRESS);
|
||||||
byte = match status {
|
byte = match status {
|
||||||
LCDStatus::LYCInterrupt => set_bit(byte, val, BitIndex::I6),
|
LCDStatus::LYCInterrupt => set_bit(byte, val, BitIndex::I6),
|
||||||
LCDStatus::Mode2OAMInterrupt => set_bit(byte, val, BitIndex::I5),
|
LCDStatus::Mode2OAMInterrupt => set_bit(byte, val, BitIndex::I5),
|
||||||
LCDStatus::Mode1VBlankInterrupt => set_bit(byte, val, BitIndex::I4),
|
LCDStatus::Mode1VBlankInterrupt => set_bit(byte, val, BitIndex::I4),
|
||||||
LCDStatus::Mode0HBlankInterrupt => set_bit(byte, val, BitIndex::I3),
|
LCDStatus::Mode0HBlankInterrupt => set_bit(byte, val, BitIndex::I3),
|
||||||
LCDStatus::LYCFlag => set_bit(byte, val, BitIndex::I2),
|
LCDStatus::LYCFlag => set_bit(byte, val, BitIndex::I2),
|
||||||
LCDStatus::ModeFlag(mode) => match mode {
|
LCDStatus::ModeFlag(mode) => match mode {
|
||||||
LCDStatusModeFlag::HBlank => (byte & 0b11111100) | 0,
|
LCDStatusModeFlag::HBlank => (byte & 0b11111100) | 0,
|
||||||
LCDStatusModeFlag::VBlank => (byte & 0b11111100) | 1,
|
LCDStatusModeFlag::VBlank => (byte & 0b11111100) | 1,
|
||||||
LCDStatusModeFlag::SearchingOAM => (byte & 0b11111100) | 2,
|
LCDStatusModeFlag::SearchingOAM => (byte & 0b11111100) | 2,
|
||||||
LCDStatusModeFlag::TransferringToLCD => (byte & 0b11111100) | 3,
|
LCDStatusModeFlag::TransferringToLCD => (byte & 0b11111100) | 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
bus.write(LCD_STATUS_ADDRESS, byte);
|
bus.write(LCD_STATUS_ADDRESS, byte);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_line(&mut self, bus: &Bus) {
|
fn get_tile_bytes(x: u8, y: u8, tile_number: TileNumber, default_method: bool, bus: &Bus) -> (u8, u8) {
|
||||||
|
let index_x = x as u16 / 8;
|
||||||
|
let index_y = (y as u16 / 8) * 32;
|
||||||
|
let index = index_x + index_y;
|
||||||
|
let tile_line = (y).rem_euclid(8) * 2;
|
||||||
|
let tile_number = match tile_number {
|
||||||
|
TileNumber::Base(base) => bus.read(base + index as u16),
|
||||||
|
TileNumber::Absolute(num) => bus.read(0x8000 + num as u16),
|
||||||
|
|
||||||
|
} as u16;
|
||||||
|
let addr = if default_method {
|
||||||
|
0x8000 + tile_line as u16 + (tile_number * 16)
|
||||||
|
} else {
|
||||||
|
let tile_number = (tile_number as i8) as i16;
|
||||||
|
let tile_line = tile_line as i16;
|
||||||
|
let base = (0x9000 as u16) as i16;
|
||||||
|
(base + tile_line + (tile_number * 16)) as u16
|
||||||
|
};
|
||||||
|
|
||||||
|
(bus.read(addr), bus.read(addr + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_window_pixel(lcd_x: u8, bus: &Bus) -> Option<Pixel> {
|
||||||
|
let lcd_y = PPU::get_lcd_y(bus);
|
||||||
|
let window_x = (PPU::get_window_x(bus) as i8 - 7) as u8;
|
||||||
|
let window_y = PPU::get_window_y(bus);
|
||||||
|
|
||||||
|
if window_x != lcd_x || window_y != lcd_y {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = lcd_x - window_x;
|
||||||
|
let y = lcd_y - window_x;
|
||||||
|
|
||||||
|
let default_mode = PPU::get_lcd_control(bus, LCDControl::TileAddressMode);
|
||||||
|
let tilemap_area = match PPU::get_lcd_control(bus, LCDControl::WindowTileMapAddress) {
|
||||||
|
true => 0x9C00,
|
||||||
|
false => 0x9800,
|
||||||
|
};
|
||||||
|
let (tile_byte_1, tile_byte_2) = PPU::get_tile_bytes(x, y, TileNumber::Base(tilemap_area), default_mode, bus);
|
||||||
|
|
||||||
|
let palette = bus.read(BACKGROUND_PALETTE_ADDRESS);
|
||||||
|
let pixels = PPU::get_byte_pixels(tile_byte_1, tile_byte_2, palette);
|
||||||
|
|
||||||
|
Some(pixels[(x as usize).rem_euclid(8)])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_line(&mut self, bus: &Bus) {
|
||||||
|
let palette = bus.read(BACKGROUND_PALETTE_ADDRESS);
|
||||||
let lcd_y = PPU::get_lcd_y(bus);
|
let lcd_y = PPU::get_lcd_y(bus);
|
||||||
if lcd_y as u32 >= LCD_HEIGHT {
|
if lcd_y as u32 >= LCD_HEIGHT {
|
||||||
return;
|
return;
|
||||||
@ -227,26 +306,40 @@ impl PPU {
|
|||||||
while (lcd_x as u32) < LCD_WIDTH {
|
while (lcd_x as u32) < LCD_WIDTH {
|
||||||
let y = lcd_y.wrapping_add(PPU::get_scroll_y(bus));
|
let y = lcd_y.wrapping_add(PPU::get_scroll_y(bus));
|
||||||
let x = lcd_x.wrapping_add(PPU::get_scroll_x(bus));
|
let x = lcd_x.wrapping_add(PPU::get_scroll_x(bus));
|
||||||
let index_x = x as u16 / 8;
|
|
||||||
let index_y = (y as u16 / 8) * 32;
|
|
||||||
let index = index_x + index_y;
|
|
||||||
let tile_line = (y).rem_euclid(8) * 2;
|
|
||||||
let tile_number = bus.read(0x9800 + index as u16) as u16;
|
|
||||||
let addr = 0x8000 + tile_line as u16 + (tile_number * 16);
|
|
||||||
|
|
||||||
let tile_byte_1 = bus.read(addr);
|
let default_mode = PPU::get_lcd_control(bus, LCDControl::TileAddressMode);
|
||||||
let tile_byte_2 = bus.read(addr + 1);
|
let tilemap_area = match PPU::get_lcd_control(bus, LCDControl::BackgroundTileMapAddress) {
|
||||||
|
true => 0x9C00,
|
||||||
|
false => 0x9800,
|
||||||
|
};
|
||||||
|
let (tile_byte_1, tile_byte_2) = PPU::get_tile_bytes(x, y, TileNumber::Base(tilemap_area), default_mode, bus);
|
||||||
|
|
||||||
let pixels = PPU::get_byte_pixels(tile_byte_1, tile_byte_2);
|
let bg_pixels = PPU::get_byte_pixels(tile_byte_1, tile_byte_2, palette);
|
||||||
|
|
||||||
for pixel in pixels {
|
for pixel in bg_pixels {
|
||||||
let idx = lcd_x as usize + (lcd_y as usize * LCD_WIDTH as usize);
|
let idx = lcd_x as usize + (lcd_y as usize * LCD_WIDTH as usize);
|
||||||
self.rgba_frame[idx] = PPU::get_rgba(pixel);
|
self.rgba_frame[idx] = PPU::get_rgba(pixel);
|
||||||
|
if PPU::get_lcd_control(bus, LCDControl::WindowEnable) {
|
||||||
|
if let Some(window_pixel) = PPU::get_window_pixel(lcd_x, bus) {
|
||||||
|
self.rgba_frame[idx] = PPU::get_rgba(window_pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lcd_x += 1;
|
lcd_x += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_palette(index: u8, palette_byte: u8) -> u8 {
|
||||||
|
match index {
|
||||||
|
0b00 => palette_byte & 0b11,
|
||||||
|
0b01 => (palette_byte >> 2) & 0b11,
|
||||||
|
0b10 => (palette_byte >> 4) & 0b11,
|
||||||
|
0b11 => (palette_byte >> 6) & 0b11,
|
||||||
|
_ => 0b00,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_pixel(two_bit_pixel: u8) -> Pixel {
|
fn get_pixel(two_bit_pixel: u8) -> Pixel {
|
||||||
match two_bit_pixel {
|
match two_bit_pixel {
|
||||||
0x00 => Pixel::White,
|
0x00 => Pixel::White,
|
||||||
@ -266,17 +359,17 @@ impl PPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_byte_pixels(byte1: u8, byte2: u8) -> [Pixel; 8] {
|
fn get_byte_pixels(byte1: u8, byte2: u8, palette: u8) -> [Pixel; 8] {
|
||||||
let mut pixels: [Pixel; 8] = [Pixel::White; 8];
|
[
|
||||||
pixels[0] = PPU::get_pixel(((get_bit(byte1, BitIndex::I7) as u8) << 1) | (get_bit(byte2, BitIndex::I7) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 7) & 0b01) | ((byte2 >> 6) & 0b10), palette)),
|
||||||
pixels[1] = PPU::get_pixel(((get_bit(byte1, BitIndex::I6) as u8) << 1) | (get_bit(byte2, BitIndex::I6) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 6) & 0b01) | ((byte2 >> 5) & 0b10), palette)),
|
||||||
pixels[2] = PPU::get_pixel(((get_bit(byte1, BitIndex::I5) as u8) << 1) | (get_bit(byte2, BitIndex::I5) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 5) & 0b01) | ((byte2 >> 4) & 0b10), palette)),
|
||||||
pixels[3] = PPU::get_pixel(((get_bit(byte1, BitIndex::I4) as u8) << 1) | (get_bit(byte2, BitIndex::I4) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 4) & 0b01) | ((byte2 >> 3) & 0b10), palette)),
|
||||||
pixels[4] = PPU::get_pixel(((get_bit(byte1, BitIndex::I3) as u8) << 1) | (get_bit(byte2, BitIndex::I3) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 3) & 0b01) | ((byte2 >> 2) & 0b10), palette)),
|
||||||
pixels[5] = PPU::get_pixel(((get_bit(byte1, BitIndex::I2) as u8) << 1) | (get_bit(byte2, BitIndex::I2) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 2) & 0b01) | ((byte2 >> 1) & 0b10), palette)),
|
||||||
pixels[6] = PPU::get_pixel(((get_bit(byte1, BitIndex::I1) as u8) << 1) | (get_bit(byte2, BitIndex::I1) as u8));
|
PPU::get_pixel(PPU::get_palette(((byte1 >> 1) & 0b01) | (byte2 & 0b10), palette)),
|
||||||
pixels[7] = PPU::get_pixel(((get_bit(byte1, BitIndex::I0) as u8) << 1) | (get_bit(byte2, BitIndex::I0) as u8));
|
PPU::get_pixel(PPU::get_palette((byte1 & 0b01) | ((byte2 << 1) & 0b10), palette)),
|
||||||
pixels
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rgba_frame(&self) -> &[[u8; 4]; FRAME_BUFFER_LENGTH as usize] {
|
pub fn get_rgba_frame(&self) -> &[[u8; 4]; FRAME_BUFFER_LENGTH as usize] {
|
||||||
|
@ -47,6 +47,8 @@ pub fn start_eventloop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emulator.handle_input(&input);
|
||||||
|
|
||||||
// Resize the window
|
// Resize the window
|
||||||
if let Some(size) = input.window_resized() {
|
if let Some(size) = input.window_resized() {
|
||||||
pixels.resize_surface(size.width, size.height);
|
pixels.resize_surface(size.width, size.height);
|
||||||
|
76
src/timer.rs
Normal file
76
src/timer.rs
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
use crate::cpu::{Interrupt, Cycles};
|
||||||
|
use crate::bus::Bus;
|
||||||
|
use crate::utils::{
|
||||||
|
BitIndex,
|
||||||
|
get_bit,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const TIMER_DIVIDER_REGISTER_ADDRESS: u16 = 0xFF04;
|
||||||
|
pub const TIMER_COUNTER_ADDRESS: u16 = 0xFF05;
|
||||||
|
pub const TIMER_MODULO_ADDRESS: u16 = 0xFF06;
|
||||||
|
pub const TIMER_CONTROL_ADDRESS: u16 = 0xFF07;
|
||||||
|
|
||||||
|
pub struct Timer {
|
||||||
|
cycles: Cycles,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Timer {
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
cycles: Cycles(0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn increment_cycles(&mut self, cycles: Cycles) {
|
||||||
|
self.cycles.0 += cycles.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset_cycles(&mut self) {
|
||||||
|
self.cycles.0 = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn do_cycles(&mut self, bus: &mut Bus, cycles: Cycles) {
|
||||||
|
let mut count = 0;
|
||||||
|
while count < cycles.to_t() {
|
||||||
|
self.cycle(bus);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cycle(&mut self, bus: &mut Bus) {
|
||||||
|
let div = bus.read(TIMER_DIVIDER_REGISTER_ADDRESS);
|
||||||
|
bus.write(TIMER_DIVIDER_REGISTER_ADDRESS, div.wrapping_add(1));
|
||||||
|
|
||||||
|
if Timer::is_timer_enabled(bus) {
|
||||||
|
let tima = bus.read(TIMER_COUNTER_ADDRESS);
|
||||||
|
let tima_rate = Timer::get_tima_rate(bus);
|
||||||
|
if self.cycles.0 >= tima_rate {
|
||||||
|
if tima.checked_add(1) == None {
|
||||||
|
bus.write(TIMER_COUNTER_ADDRESS, bus.read(TIMER_MODULO_ADDRESS));
|
||||||
|
bus.set_interrupt_flag(Interrupt::Timer, false);
|
||||||
|
} else {
|
||||||
|
bus.write(TIMER_COUNTER_ADDRESS, tima.wrapping_add(1));
|
||||||
|
}
|
||||||
|
self.reset_cycles();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.increment_cycles(Cycles(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_timer_enabled(bus: &Bus) -> bool {
|
||||||
|
get_bit(bus.read(TIMER_CONTROL_ADDRESS), BitIndex::I2)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_tima_rate(bus: &Bus) -> usize {
|
||||||
|
let clock_select = bus.read(TIMER_CONTROL_ADDRESS) & 0b0000_0011;
|
||||||
|
match clock_select {
|
||||||
|
0b00 => 16,
|
||||||
|
0b01 => 64,
|
||||||
|
0b10 => 256,
|
||||||
|
0b11 => 1024,
|
||||||
|
_ => 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user