As good as working
This commit is contained in:
parent
fc15b398ba
commit
2367e891a4
172
src/cpu.rs
172
src/cpu.rs
@ -37,8 +37,25 @@ pub struct CPU {
|
|||||||
halted: bool,
|
halted: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_u16(bytes: Box<[u8]>) -> u16 {
|
fn to_u16(args: Args) -> u16 {
|
||||||
(bytes[1] as u16) << 8 | (bytes[0] as u16)
|
match args {
|
||||||
|
Args::Double(a, b) => (a as u16) | ((b as u16) << 8),
|
||||||
|
_ => panic!("to_u16 only works with Args::Double")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Args {
|
||||||
|
Single(u8),
|
||||||
|
Double(u8, u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
fn single_val(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
&Args::Single(x) => x,
|
||||||
|
_ => panic!("single_val only works with Args::Single"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CPU {
|
impl CPU {
|
||||||
@ -61,13 +78,22 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn load_args(&mut self, num_args: u8) -> Box<[u8]> {
|
fn load_args(&mut self, num_args: u8) -> Args {
|
||||||
let mut args = Vec::new();
|
match num_args {
|
||||||
for i in 0..num_args {
|
1 => {
|
||||||
args.push(self.read_byte(self.ip + i as u16));
|
let val = self.read_byte(self.ip);
|
||||||
|
self.ip += 1;
|
||||||
|
Args::Single(val)
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
let b1 = self.read_byte(self.ip);
|
||||||
|
self.ip += 1;
|
||||||
|
let b2 = self.read_byte(self.ip);
|
||||||
|
self.ip += 1;
|
||||||
|
Args::Double(b1, b2)
|
||||||
|
}
|
||||||
|
_ => panic!("load_args only supports two bytes")
|
||||||
}
|
}
|
||||||
self.ip += num_args as u16;
|
|
||||||
args.into_boxed_slice()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@ -572,7 +598,7 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn call_condition(&mut self, cond_str: String, cond: bool) -> u8 {
|
fn call_condition(&mut self, cond_str: &'static str, cond: bool) -> u8 {
|
||||||
let dst = to_u16(self.load_args(2));
|
let dst = to_u16(self.load_args(2));
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("CALL {} {:04X}", cond_str, dst);
|
println!("CALL {} {:04X}", cond_str, dst);
|
||||||
@ -586,7 +612,7 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn ret_condition(&mut self, cond_str: String, cond: bool) -> u8 {
|
fn ret_condition(&mut self, cond_str: &'static str, cond: bool) -> u8 {
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("RET {}", cond_str);
|
println!("RET {}", cond_str);
|
||||||
}
|
}
|
||||||
@ -609,8 +635,8 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn jmp_r_condition(&mut self, cond_str: String, cond: bool) -> u8 {
|
fn jmp_r_condition(&mut self, cond_str: &'static str, cond: bool) -> u8 {
|
||||||
let t = self.load_args(1)[0];
|
let t = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("JR {} {:02X}", cond_str, t);
|
println!("JR {} {:02X}", cond_str, t);
|
||||||
}
|
}
|
||||||
@ -628,7 +654,7 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn jmp_p_condition(&mut self, cond_str: String, cond: bool) -> u8 {
|
fn jmp_p_condition(&mut self, cond_str: &'static str, cond: bool) -> u8 {
|
||||||
let t = to_u16(self.load_args(2));
|
let t = to_u16(self.load_args(2));
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("JP {} {:04X}", cond_str, t);
|
println!("JP {} {:04X}", cond_str, t);
|
||||||
@ -722,7 +748,7 @@ impl CPU {
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn ld_r_v(&mut self, r: usize) -> u8 {
|
fn ld_r_v(&mut self, r: usize) -> u8 {
|
||||||
let val: u8 = self.load_args(1)[0];
|
let val: u8 = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("LD {}, {:02X}", REG_NAMES[r], val);
|
println!("LD {}, {:02X}", REG_NAMES[r], val);
|
||||||
}
|
}
|
||||||
@ -859,20 +885,35 @@ impl CPU {
|
|||||||
self.halted = false;
|
self.halted = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused_variables)]
|
||||||
pub fn run(&mut self) {
|
pub fn run(&mut self) {
|
||||||
|
let mut running_sum: i32 = 0;
|
||||||
|
const INSTRUCTIONS_PER_ROUND: usize = 500;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// Some attempt at limiting our speed
|
|
||||||
let mut cycles: i32 = 0;
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
for _ in 0 .. 1000 {
|
let mut cycles_executed: usize = 0;
|
||||||
cycles += self.run_instruction() as i32;
|
for _ in 0..INSTRUCTIONS_PER_ROUND {
|
||||||
|
cycles_executed += self.run_instruction() as usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
let gb_dur = cycles * 238;
|
// Time that the gameboy would have required to execute the
|
||||||
|
// instructions above.
|
||||||
|
//
|
||||||
|
// The gameboy has a freq of 4.194304MHz
|
||||||
|
// This means it takes it 238.418569ns to execute a single
|
||||||
|
// cycle.
|
||||||
|
let gb_dur = cycles_executed as i32 * 238;
|
||||||
let our_dur = start.elapsed().subsec_nanos() as i32;
|
let our_dur = start.elapsed().subsec_nanos() as i32;
|
||||||
let delta = gb_dur - our_dur;
|
let delta = gb_dur - our_dur;
|
||||||
if delta > (20 * 238) { // We're at least 20 cycles faster.
|
|
||||||
sleep(Duration::new(0, delta as u32));
|
let full_cycles: i32 = delta / 238;
|
||||||
|
running_sum += full_cycles;
|
||||||
|
if full_cycles > 0 {
|
||||||
|
// println!("[+] [{}] {} cycles, gb: {}, we: {}", running_sum, cycles_executed, gb_dur, our_dur);
|
||||||
|
sleep(Duration::new(0, (full_cycles - 10) as u32 * 238 as u32));
|
||||||
|
} else {
|
||||||
|
println!("[-] [{}] {} cycles, gb: {}, we: {}", running_sum, cycles_executed, gb_dur, our_dur);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -918,6 +959,16 @@ impl CPU {
|
|||||||
|
|
||||||
pub fn run_instruction(&mut self) -> u8 {
|
pub fn run_instruction(&mut self) -> u8 {
|
||||||
// self.debug = !self.interconnect.is_boot_rom();
|
// self.debug = !self.interconnect.is_boot_rom();
|
||||||
|
|
||||||
|
/*
|
||||||
|
if self.ip >= 100 && self.ip < 120 {
|
||||||
|
self.debug = true;
|
||||||
|
} else {
|
||||||
|
self.debug = false;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// self.debug = true;
|
||||||
// Check for interrupts.
|
// Check for interrupts.
|
||||||
if self.ime {
|
if self.ime {
|
||||||
self.check_interrupts(true);
|
self.check_interrupts(true);
|
||||||
@ -927,7 +978,7 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cycles: u8 = 1;
|
let mut cycles: u8 = 255;
|
||||||
let instruction: u8;
|
let instruction: u8;
|
||||||
if !self.halted {
|
if !self.halted {
|
||||||
// We need to double-check the flags
|
// We need to double-check the flags
|
||||||
@ -1017,7 +1068,7 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
|
|
||||||
0x10 => {
|
0x10 => {
|
||||||
println!("STOP 0 {:02X} not implemented.", self.load_args(1)[0]);
|
println!("STOP 0 {:02X} not implemented.", self.load_args(1).single_val());
|
||||||
4
|
4
|
||||||
},
|
},
|
||||||
0x11 => self.ld_rr_vv(REG_N_D, REG_N_E),
|
0x11 => self.ld_rr_vv(REG_N_D, REG_N_E),
|
||||||
@ -1044,8 +1095,11 @@ impl CPU {
|
|||||||
4
|
4
|
||||||
},
|
},
|
||||||
0x18 => {
|
0x18 => {
|
||||||
let dst = self.load_args(1)[0];
|
let dst = self.load_args(1).single_val();
|
||||||
self.jmp_r(dst);
|
self.jmp_r(dst);
|
||||||
|
if self.debug {
|
||||||
|
println!("JMPR {:02X}", dst);
|
||||||
|
}
|
||||||
12
|
12
|
||||||
},
|
},
|
||||||
0x19 => self.add_rr_rr(REG_N_H, REG_N_L, REG_N_D, REG_N_E),
|
0x19 => self.add_rr_rr(REG_N_H, REG_N_L, REG_N_D, REG_N_E),
|
||||||
@ -1081,7 +1135,7 @@ impl CPU {
|
|||||||
|
|
||||||
0x20 => {
|
0x20 => {
|
||||||
let c = self.flags & FLAG_Z == 0;
|
let c = self.flags & FLAG_Z == 0;
|
||||||
self.jmp_r_condition("NZ".to_owned(), c)
|
self.jmp_r_condition("NZ", c)
|
||||||
}
|
}
|
||||||
0x21 => self.ld_rr_vv(REG_N_H, REG_N_L),
|
0x21 => self.ld_rr_vv(REG_N_H, REG_N_L),
|
||||||
0x22 => {
|
0x22 => {
|
||||||
@ -1137,7 +1191,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0x28 => {
|
0x28 => {
|
||||||
let c = self.flags & FLAG_Z == FLAG_Z;
|
let c = self.flags & FLAG_Z == FLAG_Z;
|
||||||
self.jmp_r_condition("Z".to_owned(), c)
|
self.jmp_r_condition("Z", c)
|
||||||
},
|
},
|
||||||
0x29 => self.add_rr_rr(REG_N_H, REG_N_L, REG_N_H, REG_N_L),
|
0x29 => self.add_rr_rr(REG_N_H, REG_N_L, REG_N_H, REG_N_L),
|
||||||
0x2A => {
|
0x2A => {
|
||||||
@ -1166,7 +1220,7 @@ impl CPU {
|
|||||||
|
|
||||||
0x30 => {
|
0x30 => {
|
||||||
let c = self.flags & FLAG_C == 0;
|
let c = self.flags & FLAG_C == 0;
|
||||||
self.jmp_r_condition("NC".to_owned(), c)
|
self.jmp_r_condition("NC", c)
|
||||||
},
|
},
|
||||||
0x31 => {
|
0x31 => {
|
||||||
let args = self.load_args(2);
|
let args = self.load_args(2);
|
||||||
@ -1207,7 +1261,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0x38 => {
|
0x38 => {
|
||||||
let c = self.flags & FLAG_C == FLAG_C;
|
let c = self.flags & FLAG_C == FLAG_C;
|
||||||
self.jmp_r_condition("C".to_owned(), c)
|
self.jmp_r_condition("C", c)
|
||||||
},
|
},
|
||||||
0x39 => {
|
0x39 => {
|
||||||
if self.debug {
|
if self.debug {
|
||||||
@ -1379,12 +1433,12 @@ impl CPU {
|
|||||||
|
|
||||||
0xC0 => {
|
0xC0 => {
|
||||||
let c = self.flags & FLAG_Z == 0;
|
let c = self.flags & FLAG_Z == 0;
|
||||||
self.ret_condition("NZ".to_owned(), c)
|
self.ret_condition("NZ", c)
|
||||||
},
|
},
|
||||||
0xC1 => self.pop_rr(REG_N_B, REG_N_C),
|
0xC1 => self.pop_rr(REG_N_B, REG_N_C),
|
||||||
0xC2 => {
|
0xC2 => {
|
||||||
let c = self.flags & FLAG_Z == 0;
|
let c = self.flags & FLAG_Z == 0;
|
||||||
self.jmp_p_condition("NZ".to_owned(), c)
|
self.jmp_p_condition("NZ", c)
|
||||||
},
|
},
|
||||||
0xC3 => {
|
0xC3 => {
|
||||||
let dst = to_u16(self.load_args(2));
|
let dst = to_u16(self.load_args(2));
|
||||||
@ -1396,11 +1450,11 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xC4 => {
|
0xC4 => {
|
||||||
let c = self.flags & FLAG_Z == 0;
|
let c = self.flags & FLAG_Z == 0;
|
||||||
self.call_condition("NZ".to_owned(), c)
|
self.call_condition("NZ", c)
|
||||||
}
|
}
|
||||||
0xC5 => self.push_rr(REG_N_B, REG_N_C),
|
0xC5 => self.push_rr(REG_N_B, REG_N_C),
|
||||||
0xC6 => {
|
0xC6 => {
|
||||||
let val = self.load_args(1)[0];
|
let val = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("ADD A, {:02X}", val);
|
println!("ADD A, {:02X}", val);
|
||||||
}
|
}
|
||||||
@ -1412,7 +1466,7 @@ impl CPU {
|
|||||||
0xC7 => self.rst(0x00),
|
0xC7 => self.rst(0x00),
|
||||||
0xC8 => {
|
0xC8 => {
|
||||||
let c = self.flags & FLAG_Z == FLAG_Z;
|
let c = self.flags & FLAG_Z == FLAG_Z;
|
||||||
self.ret_condition("Z".to_owned(), c)
|
self.ret_condition("Z", c)
|
||||||
},
|
},
|
||||||
0xC9 => {
|
0xC9 => {
|
||||||
if self.debug {
|
if self.debug {
|
||||||
@ -1423,7 +1477,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xCA => {
|
0xCA => {
|
||||||
let c = self.flags & FLAG_Z == FLAG_Z;
|
let c = self.flags & FLAG_Z == FLAG_Z;
|
||||||
self.jmp_p_condition("Z".to_owned(), c)
|
self.jmp_p_condition("Z", c)
|
||||||
}
|
}
|
||||||
0xCB => {
|
0xCB => {
|
||||||
self.run_prefix_instruction();
|
self.run_prefix_instruction();
|
||||||
@ -1431,11 +1485,11 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xCC => {
|
0xCC => {
|
||||||
let c = self.flags & FLAG_Z == FLAG_Z;
|
let c = self.flags & FLAG_Z == FLAG_Z;
|
||||||
self.call_condition("Z".to_owned(), c)
|
self.call_condition("Z", c)
|
||||||
},
|
},
|
||||||
0xCD => self.call_condition("".to_owned(), true),
|
0xCD => self.call_condition("", true),
|
||||||
0xCE => {
|
0xCE => {
|
||||||
let arg = self.load_args(1)[0];
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("ADC A, {:02X}", arg);
|
println!("ADC A, {:02X}", arg);
|
||||||
}
|
}
|
||||||
@ -1447,21 +1501,21 @@ impl CPU {
|
|||||||
|
|
||||||
0xD0 => {
|
0xD0 => {
|
||||||
let c = self.flags & FLAG_C == 0;
|
let c = self.flags & FLAG_C == 0;
|
||||||
self.ret_condition("NC".to_owned(), c)
|
self.ret_condition("NC", c)
|
||||||
},
|
},
|
||||||
0xD1 => self.pop_rr(REG_N_D, REG_N_E),
|
0xD1 => self.pop_rr(REG_N_D, REG_N_E),
|
||||||
0xD2 => {
|
0xD2 => {
|
||||||
let c = self.flags & FLAG_C == 0;
|
let c = self.flags & FLAG_C == 0;
|
||||||
self.jmp_p_condition("NC".to_owned(), c)
|
self.jmp_p_condition("NC", c)
|
||||||
}
|
}
|
||||||
0xD3 => panic!("NON-EXISTING OPCODE"),
|
0xD3 => panic!("NON-EXISTING OPCODE"),
|
||||||
0xD4 => {
|
0xD4 => {
|
||||||
let c = self.flags & FLAG_C == 0;
|
let c = self.flags & FLAG_C == 0;
|
||||||
self.call_condition("NC".to_owned(), c)
|
self.call_condition("NC", c)
|
||||||
}
|
}
|
||||||
0xD5 => self.push_rr(REG_N_D, REG_N_E),
|
0xD5 => self.push_rr(REG_N_D, REG_N_E),
|
||||||
0xD6 => {
|
0xD6 => {
|
||||||
let val = self.load_args(1)[0];
|
let val = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("SUB {:02X}", val);
|
println!("SUB {:02X}", val);
|
||||||
}
|
}
|
||||||
@ -1472,7 +1526,7 @@ impl CPU {
|
|||||||
0xD7 => self.rst(0x10),
|
0xD7 => self.rst(0x10),
|
||||||
0xD8 => {
|
0xD8 => {
|
||||||
let c = self.flags & FLAG_C == FLAG_C;
|
let c = self.flags & FLAG_C == FLAG_C;
|
||||||
self.ret_condition("C".to_owned(), c)
|
self.ret_condition("C", c)
|
||||||
},
|
},
|
||||||
0xD9 => {
|
0xD9 => {
|
||||||
if self.debug {
|
if self.debug {
|
||||||
@ -1482,16 +1536,16 @@ impl CPU {
|
|||||||
}
|
}
|
||||||
0xDA => {
|
0xDA => {
|
||||||
let c = self.flags & FLAG_C == FLAG_C;
|
let c = self.flags & FLAG_C == FLAG_C;
|
||||||
self.jmp_p_condition("C".to_owned(), c)
|
self.jmp_p_condition("C", c)
|
||||||
},
|
},
|
||||||
0xDB => panic!("NON-EXISTING OPCODE"),
|
0xDB => panic!("NON-EXISTING OPCODE"),
|
||||||
0xDC => {
|
0xDC => {
|
||||||
let c = self.flags & FLAG_C == FLAG_C;
|
let c = self.flags & FLAG_C == FLAG_C;
|
||||||
self.call_condition("C".to_owned(), c)
|
self.call_condition("C", c)
|
||||||
}
|
}
|
||||||
0xDD => panic!("NON-EXISTING OPCODE"),
|
0xDD => panic!("NON-EXISTING OPCODE"),
|
||||||
0xDE => {
|
0xDE => {
|
||||||
let arg = self.load_args(1)[0];
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("SBC {:02X}", arg);
|
println!("SBC {:02X}", arg);
|
||||||
}
|
}
|
||||||
@ -1501,11 +1555,11 @@ impl CPU {
|
|||||||
0xDF => self.rst(0x18),
|
0xDF => self.rst(0x18),
|
||||||
|
|
||||||
0xE0 => {
|
0xE0 => {
|
||||||
let args = self.load_args(1);
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("LDH {:02X}, A", args[0]);
|
println!("LDH {:02X}, A", arg);
|
||||||
}
|
}
|
||||||
self.interconnect.write_byte(0xFF00 + args[0] as u16, self.regs[REG_A]);
|
self.interconnect.write_byte(0xFF00 + arg as u16, self.regs[REG_A]);
|
||||||
12
|
12
|
||||||
},
|
},
|
||||||
0xE1 => self.pop_rr(REG_N_H, REG_N_L),
|
0xE1 => self.pop_rr(REG_N_H, REG_N_L),
|
||||||
@ -1520,7 +1574,7 @@ impl CPU {
|
|||||||
0xE3 | 0xE4 => panic!("NON-EXISTING OPCODE"),
|
0xE3 | 0xE4 => panic!("NON-EXISTING OPCODE"),
|
||||||
0xE5 => self.push_rr(REG_N_H, REG_N_L),
|
0xE5 => self.push_rr(REG_N_H, REG_N_L),
|
||||||
0xE6 => {
|
0xE6 => {
|
||||||
let val = self.load_args(1)[0];
|
let val = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("AND {:02X}", val);
|
println!("AND {:02X}", val);
|
||||||
}
|
}
|
||||||
@ -1535,7 +1589,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xE7 => self.rst(0x20),
|
0xE7 => self.rst(0x20),
|
||||||
0xE8 => {
|
0xE8 => {
|
||||||
let arg = self.load_args(1)[0] as i8;
|
let arg = self.load_args(1).single_val() as i8;
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("ADD SP, {:02X}", arg);
|
println!("ADD SP, {:02X}", arg);
|
||||||
}
|
}
|
||||||
@ -1570,7 +1624,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xEB ... 0xED => panic!("NON-EXISTING OPCODE"),
|
0xEB ... 0xED => panic!("NON-EXISTING OPCODE"),
|
||||||
0xEE => {
|
0xEE => {
|
||||||
let arg = self.load_args(1)[0];
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("XOR {:02X}", arg);
|
println!("XOR {:02X}", arg);
|
||||||
}
|
}
|
||||||
@ -1585,11 +1639,11 @@ impl CPU {
|
|||||||
0xEF => self.rst(0x28),
|
0xEF => self.rst(0x28),
|
||||||
|
|
||||||
0xF0 => {
|
0xF0 => {
|
||||||
let args = self.load_args(1);
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("LDH A, {:02X}", args[0]);
|
println!("LDH A, {:02X}", arg);
|
||||||
}
|
}
|
||||||
self.regs[REG_A] = self.interconnect.read_byte(0xFF00 + args[0] as u16);
|
self.regs[REG_A] = self.interconnect.read_byte(0xFF00 + arg as u16);
|
||||||
12
|
12
|
||||||
},
|
},
|
||||||
0xF1 => self.pop_rr(REG_N_A, REG_N_F),
|
0xF1 => self.pop_rr(REG_N_A, REG_N_F),
|
||||||
@ -1611,7 +1665,7 @@ impl CPU {
|
|||||||
0xF4 => panic!("NON-EXISTING OPCODE"),
|
0xF4 => panic!("NON-EXISTING OPCODE"),
|
||||||
0xF5 => self.push_rr(REG_N_A, REG_N_F),
|
0xF5 => self.push_rr(REG_N_A, REG_N_F),
|
||||||
0xF6 => {
|
0xF6 => {
|
||||||
let val = self.load_args(1)[0];
|
let val = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("OR {:02X}", val);
|
println!("OR {:02X}", val);
|
||||||
}
|
}
|
||||||
@ -1625,7 +1679,7 @@ impl CPU {
|
|||||||
},
|
},
|
||||||
0xF7 => self.rst(0x30),
|
0xF7 => self.rst(0x30),
|
||||||
0xF8 => {
|
0xF8 => {
|
||||||
let arg = self.load_args(1)[0] as i8;
|
let arg = self.load_args(1).single_val() as i8;
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("LD HL, SP+{:02X}", arg);
|
println!("LD HL, SP+{:02X}", arg);
|
||||||
}
|
}
|
||||||
@ -1672,12 +1726,12 @@ impl CPU {
|
|||||||
0xFC | 0xFD => panic!("NON-EXISTING OPCODE"),
|
0xFC | 0xFD => panic!("NON-EXISTING OPCODE"),
|
||||||
|
|
||||||
0xFE => {
|
0xFE => {
|
||||||
let args = self.load_args(1);
|
let arg = self.load_args(1).single_val();
|
||||||
if self.debug {
|
if self.debug {
|
||||||
println!("CP {:02X}", args[0]);
|
println!("CP {:02X}", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cp_r(args[0]);
|
self.cp_r(arg);
|
||||||
8
|
8
|
||||||
},
|
},
|
||||||
0xFF => self.rst(0x38),
|
0xFF => self.rst(0x38),
|
||||||
|
|||||||
@ -199,7 +199,7 @@ impl Interconnect {
|
|||||||
// TODO: if some flag set, use bios, otherwise only use rom
|
// TODO: if some flag set, use bios, otherwise only use rom
|
||||||
// For now, just use bios
|
// For now, just use bios
|
||||||
match addr {
|
match addr {
|
||||||
0x0000 ... 0x100 => {
|
0x0000 ... 0xFF => {
|
||||||
if self.disable_bootrom == 0 {
|
if self.disable_bootrom == 0 {
|
||||||
self.bios[addr as usize]
|
self.bios[addr as usize]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
126
src/sound/mod.rs
126
src/sound/mod.rs
@ -8,17 +8,15 @@ use self::pulse_simple::Playback;
|
|||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
//use std::f64::consts::PI;
|
const OUTPUT_SAMPLE_RATE: usize = 48100;
|
||||||
|
//const OUTPUT_SAMPLE_RATE: usize = 32768;
|
||||||
//const TICK_RATE: usize = 4194304;
|
|
||||||
const OUTPUT_SAMPLE_RATE: usize = 32768;
|
|
||||||
//const OUTPUT_SAMPLE_RATE: usize = 1024;
|
|
||||||
//const TICKS_PER_SAMPLE: usize = TICK_RATE / OUTPUT_SAMPLE_RATE;
|
|
||||||
|
|
||||||
|
/// Represents an 'audio module', transforming an input sample to an output sample
|
||||||
trait AudioModule<T> {
|
trait AudioModule<T> {
|
||||||
fn transform(&self, sample: T) -> T;
|
fn transform(&self, sample: T) -> T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clockable audio components
|
||||||
trait AudioComponent {
|
trait AudioComponent {
|
||||||
fn clock(&mut self);
|
fn clock(&mut self);
|
||||||
}
|
}
|
||||||
@ -31,26 +29,33 @@ struct Channel1 {
|
|||||||
envelope: envelope::VolumeEnvelope,
|
envelope: envelope::VolumeEnvelope,
|
||||||
|
|
||||||
tick_state: u8,
|
tick_state: u8,
|
||||||
|
|
||||||
|
stored_regs: [u8; 5]
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel1 {
|
impl Channel1 {
|
||||||
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
||||||
match addr {
|
match addr {
|
||||||
0xFF10 => {
|
0xFF10 => {
|
||||||
println!("Channel1: Sweep not implemented yet :<");
|
self.wave_gen.set_sweep_reg(val);
|
||||||
|
self.stored_regs[0] = val;
|
||||||
}
|
}
|
||||||
0xFF11 => {
|
0xFF11 => {
|
||||||
self.length_counter.set_length(val & 0x3F);
|
self.length_counter.set_length(val & 0x3F);
|
||||||
self.wave_gen.set_duty_cycle(val >> 6);
|
self.wave_gen.set_duty_cycle(val >> 6);
|
||||||
|
self.stored_regs[1] = val;
|
||||||
}
|
}
|
||||||
0xFF12 => {
|
0xFF12 => {
|
||||||
self.envelope.set_register(val);
|
self.envelope.set_register(val);
|
||||||
|
self.stored_regs[2] = val;
|
||||||
}
|
}
|
||||||
0xFF13 => {
|
0xFF13 => {
|
||||||
// Set lower frequency
|
// Set lower frequency
|
||||||
self.wave_gen.set_lower_freq(val);
|
self.wave_gen.set_lower_freq(val);
|
||||||
|
self.stored_regs[3] = val;
|
||||||
}
|
}
|
||||||
0xFF14 => {
|
0xFF14 => {
|
||||||
|
self.stored_regs[4] = val;
|
||||||
// Set higher
|
// Set higher
|
||||||
self.wave_gen.set_higher_freq(val);
|
self.wave_gen.set_higher_freq(val);
|
||||||
|
|
||||||
@ -67,7 +72,8 @@ impl Channel1 {
|
|||||||
|
|
||||||
pub fn read_byte(&self, addr: u16) -> u8 {
|
pub fn read_byte(&self, addr: u16) -> u8 {
|
||||||
match addr {
|
match addr {
|
||||||
_ => panic!("Channel1 does not support reading ({:04X}", addr),
|
0xFF10...0xFF14 => self.stored_regs[(addr - 0xFF10) as usize],
|
||||||
|
_ => {println!("Channel1 does not support reading ({:04X})", addr); 0},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +94,7 @@ impl Channel1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.tick_state == 2 || self.tick_state == 6 {
|
if self.tick_state == 2 || self.tick_state == 6 {
|
||||||
// TODO: Sweep
|
self.wave_gen.sweep_clock();
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tick_state == 7 {
|
if self.tick_state == 7 {
|
||||||
@ -144,7 +150,7 @@ impl Channel2 {
|
|||||||
|
|
||||||
pub fn read_byte(&self, addr: u16) -> u8 {
|
pub fn read_byte(&self, addr: u16) -> u8 {
|
||||||
match addr {
|
match addr {
|
||||||
_ => panic!("Channel2 does not support reading ({:04X}", addr),
|
_ => {println!("Channel2 does not support reading ({:04X})", addr); 0},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -192,7 +198,7 @@ struct Channel3 {
|
|||||||
|
|
||||||
impl Channel3 {
|
impl Channel3 {
|
||||||
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
||||||
println!("C3 WB: {:04X} = {:02X} ({:08b})", addr, val, val);
|
// println!("C3 WB: {:04X} = {:02X} ({:08b})", addr, val, val);
|
||||||
match addr {
|
match addr {
|
||||||
0xFF1A => {
|
0xFF1A => {
|
||||||
self.wave_gen.enabled = (val & (1 << 7)) > 0;
|
self.wave_gen.enabled = (val & (1 << 7)) > 0;
|
||||||
@ -226,7 +232,7 @@ impl Channel3 {
|
|||||||
|
|
||||||
pub fn read_byte(&self, addr: u16) -> u8 {
|
pub fn read_byte(&self, addr: u16) -> u8 {
|
||||||
match addr {
|
match addr {
|
||||||
_ => panic!("Channel3 does not support reading ({:04X}", addr),
|
_ => panic!("Channel3 does not support reading ({:04X})", addr),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,8 +243,7 @@ impl Channel3 {
|
|||||||
pub fn sample(&self) -> u8 {
|
pub fn sample(&self) -> u8 {
|
||||||
let input = self.wave_gen.sample();
|
let input = self.wave_gen.sample();
|
||||||
// Follow the chain
|
// Follow the chain
|
||||||
let input = self.length_counter.transform(input);
|
// let input = self.length_counter.transform(input);
|
||||||
// self.envelope.transform(input)
|
|
||||||
input
|
input
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +296,10 @@ impl SoundManager {
|
|||||||
|
|
||||||
pub fn launch_thread(&self) {
|
pub fn launch_thread(&self) {
|
||||||
let obj = self.sound_object.clone();
|
let obj = self.sound_object.clone();
|
||||||
thread::spawn(move || {
|
if false {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thread::Builder::new().name("Audio".into()).spawn(move || {
|
||||||
// PulseAudio playback object
|
// PulseAudio playback object
|
||||||
let playback = Playback::new("GBC", "Output", None, (OUTPUT_SAMPLE_RATE) as _ );
|
let playback = Playback::new("GBC", "Output", None, (OUTPUT_SAMPLE_RATE) as _ );
|
||||||
|
|
||||||
@ -299,20 +307,9 @@ impl SoundManager {
|
|||||||
let mut counter = 0;
|
let mut counter = 0;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
{
|
let (s1, s2) = {
|
||||||
let mut c_obj = obj.lock().unwrap();
|
let mut c_obj = obj.lock().unwrap();
|
||||||
|
|
||||||
c_obj.channel1.sample_clock();
|
|
||||||
c_obj.channel2.sample_clock();
|
|
||||||
c_obj.channel3.sample_clock();
|
|
||||||
|
|
||||||
// let s = c_obj.sample();
|
|
||||||
let s1 = c_obj.channel1.sample();
|
|
||||||
let s2 = c_obj.channel2.sample();
|
|
||||||
let s3 = c_obj.channel3.sample();
|
|
||||||
let samps = [[s3 as u8, s3 as u8]];
|
|
||||||
playback.write(&samps[..]);
|
|
||||||
|
|
||||||
// Check for 512 Hz timer
|
// Check for 512 Hz timer
|
||||||
if counter >= (OUTPUT_SAMPLE_RATE / 512) {
|
if counter >= (OUTPUT_SAMPLE_RATE / 512) {
|
||||||
c_obj.clock();
|
c_obj.clock();
|
||||||
@ -320,11 +317,18 @@ impl SoundManager {
|
|||||||
} else {
|
} else {
|
||||||
counter += 1;
|
counter += 1;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Sample clock
|
||||||
|
c_obj.sample_clock();
|
||||||
|
|
||||||
|
// Get sample
|
||||||
|
c_obj.sample()
|
||||||
|
};
|
||||||
|
let samps = [[s1, s2]];
|
||||||
// No sleep needed, it seems like the playback.write is blocking
|
// No sleep needed, it seems like the playback.write is blocking
|
||||||
// thread::sleep(time::Duration::new(0, sleep_duration));
|
playback.write(&samps[..]);
|
||||||
}
|
}
|
||||||
});
|
}).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -339,9 +343,65 @@ impl Sound {
|
|||||||
self.channel3.clock();
|
self.channel3.clock();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sample(&self) -> u8 {
|
pub fn sample_clock(&mut self) {
|
||||||
// TODO: Mixing
|
self.channel1.sample_clock();
|
||||||
(self.channel1.sample() >> 1) + (self.channel2.sample() >> 1) + (self.channel3.sample() >> 1)
|
self.channel2.sample_clock();
|
||||||
|
self.channel3.sample_clock();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample(&self) -> (u8, u8) {
|
||||||
|
// Some basic mixing
|
||||||
|
// Output value
|
||||||
|
let mut s = [0u16; 2];
|
||||||
|
let s01_volume = self.sound_channel_volume_control & 7;
|
||||||
|
let s02_volume = (self.sound_channel_volume_control >> 4) & 7;
|
||||||
|
|
||||||
|
let c1_sample = self.channel1.sample();
|
||||||
|
let c2_sample = self.channel2.sample();
|
||||||
|
let c3_sample = self.channel3.sample();
|
||||||
|
let c4_sample = 0;
|
||||||
|
|
||||||
|
if self.enabled != 0 {
|
||||||
|
if s01_volume > 0 {
|
||||||
|
if self.sound_output_terminal_selector & 1 > 0 {
|
||||||
|
// Output Channel1
|
||||||
|
s[0] += c1_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 2 > 0 && false {
|
||||||
|
// Output Channel2
|
||||||
|
s[0] += c2_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 4 > 0 {
|
||||||
|
// Output Channel3
|
||||||
|
s[0] += c3_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 8 > 0 {
|
||||||
|
// Output Channel4
|
||||||
|
s[0] += c4_sample as _;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s02_volume > 0 {
|
||||||
|
if self.sound_output_terminal_selector & 0x10 > 0 && false {
|
||||||
|
// Output Channel1
|
||||||
|
s[1] += c1_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 0x20 > 0 {
|
||||||
|
// Output Channel2
|
||||||
|
s[1] += c2_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 0x40 > 0 {
|
||||||
|
// Output Channel3
|
||||||
|
s[1] += c3_sample as _;
|
||||||
|
}
|
||||||
|
if self.sound_output_terminal_selector & 0x80 > 0 {
|
||||||
|
// Output Channel4
|
||||||
|
s[1] += c4_sample as _;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// (self.channel1.sample() >> 1) + (self.channel2.sample() >> 1) + (self.channel3.sample() >> 1)
|
||||||
|
((s[0] >> 2) as _, (s[1] >> 2) as _)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
||||||
|
|||||||
@ -23,6 +23,18 @@ pub struct SquareWaveGenerator {
|
|||||||
duty_cycle: DutyCycle,
|
duty_cycle: DutyCycle,
|
||||||
|
|
||||||
gb_reg_freq: u16,
|
gb_reg_freq: u16,
|
||||||
|
|
||||||
|
// Sweep settings (if existing)
|
||||||
|
sweep_change_period: u8,
|
||||||
|
// selects the inc/dec mode of the sweep. Dec if set
|
||||||
|
sweep_dec: bool,
|
||||||
|
// Sweep speed (n/128th)
|
||||||
|
sweep_change: u8,
|
||||||
|
|
||||||
|
// Current sweep clock cycle
|
||||||
|
sweep_clock: u8,
|
||||||
|
|
||||||
|
// sweep_freq_shadow: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SquareWaveGenerator {
|
impl SquareWaveGenerator {
|
||||||
@ -42,6 +54,35 @@ impl SquareWaveGenerator {
|
|||||||
self.update_frequency();
|
self.update_frequency();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_sweep_reg(&mut self, reg: u8) {
|
||||||
|
//Bit 6-4 - Sweep Time
|
||||||
|
//Bit 3 - Sweep Increase/Decrease
|
||||||
|
// 0: Addition (frequency increases)
|
||||||
|
// 1: Subtraction (frequency decreases)
|
||||||
|
//Bit 2-0 - Number of sweep shift (n: 0-7)
|
||||||
|
self.sweep_change_period = (reg >> 4) & 7;
|
||||||
|
self.sweep_dec = reg & (1 << 3) > 0;
|
||||||
|
self.sweep_change = reg & 7;
|
||||||
|
self.sweep_clock = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sweep_clock(&mut self) {
|
||||||
|
// self.sweep_freq_shadow = self.frequency;
|
||||||
|
if self.sweep_change_period > 0 {
|
||||||
|
if self.sweep_change > 0 {
|
||||||
|
if self.sweep_clock >= self.sweep_change_period {
|
||||||
|
self.sweep_clock = 0;
|
||||||
|
let change = self.frequency / (2f32.powi(self.sweep_change as _));
|
||||||
|
self.frequency += if self.sweep_dec {
|
||||||
|
change
|
||||||
|
} else {
|
||||||
|
-change
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.time = 0f32;
|
self.time = 0f32;
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/timer.rs
39
src/timer.rs
@ -11,6 +11,11 @@ pub struct Timer {
|
|||||||
gb_ticks: u16,
|
gb_ticks: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Timer speed contains 4 different scales:
|
||||||
|
// [0] = 64 = 4096 Hz
|
||||||
|
// [1] = 1 = 262144 Hz
|
||||||
|
// [2] = 4 = 65536 Hz
|
||||||
|
// [3] = 16 = 16384 Hz
|
||||||
const TIMER_SPEED: [u16; 4] = [64, 1, 4, 16];
|
const TIMER_SPEED: [u16; 4] = [64, 1, 4, 16];
|
||||||
const TIMER_ENABLE: u8 = (1 << 2);
|
const TIMER_ENABLE: u8 = (1 << 2);
|
||||||
|
|
||||||
@ -19,34 +24,38 @@ impl Timer {
|
|||||||
Timer::default()
|
Timer::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This clock is ticking at 262.144 Hz
|
||||||
fn timer_clock_tick(&mut self) {
|
fn timer_clock_tick(&mut self) {
|
||||||
// The div reg will always tick
|
// The div reg will always tick
|
||||||
self.div_tick_counter += 1;
|
self.div_tick_counter += 1;
|
||||||
if self.div_tick_counter >= TIMER_SPEED[3] {
|
|
||||||
|
// 262144 Hz / 16 = 16384 Hz
|
||||||
|
if self.div_tick_counter >= 16 {
|
||||||
self.div = self.div.wrapping_add(1);
|
self.div = self.div.wrapping_add(1);
|
||||||
self.div_tick_counter -= TIMER_SPEED[3];
|
self.div_tick_counter -= 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (self.tac & TIMER_ENABLE) == TIMER_ENABLE { // Is timer enabled?
|
// Check if the timer is enabled
|
||||||
|
if (self.tac & TIMER_ENABLE) != 0 {
|
||||||
self.tick_counter += 1;
|
self.tick_counter += 1;
|
||||||
let req_ticks = TIMER_SPEED[(self.tac & 3) as usize];
|
let req_ticks = TIMER_SPEED[(self.tac & 3) as usize];
|
||||||
|
|
||||||
if self.tick_counter >= req_ticks {
|
if self.tick_counter == req_ticks {
|
||||||
if self.tima == 0xFF {
|
if self.tima == 0xFF {
|
||||||
self.timer_interrupt = true;
|
self.timer_interrupt = true;
|
||||||
self.tima = 0;
|
self.tima = self.tma;
|
||||||
} else {
|
} else {
|
||||||
self.tima += 1;
|
self.tima += 1;
|
||||||
}
|
}
|
||||||
self.tick_counter -= req_ticks;
|
self.tick_counter = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tick(&mut self, ticks: u16) {
|
pub fn tick(&mut self, ticks: u16) {
|
||||||
// One tick 1/4.194304MHz on regular speed => 16 gb ticks
|
// One tick 1/4.194304MHz on regular speed => 16 gb ticks = prescaler
|
||||||
self.gb_ticks += ticks;
|
self.gb_ticks += ticks;
|
||||||
// If we're in GBC fast mode, we'd require 32 ticks instead of 16
|
|
||||||
while self.gb_ticks >= 16 {
|
while self.gb_ticks >= 16 {
|
||||||
self.timer_clock_tick();
|
self.timer_clock_tick();
|
||||||
self.gb_ticks -= 16;
|
self.gb_ticks -= 16;
|
||||||
@ -54,26 +63,26 @@ impl Timer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
pub fn write_byte(&mut self, addr: u16, val: u8) {
|
||||||
|
// println!("Timer WR: {:04X} = {:02X}", addr, val);
|
||||||
match addr {
|
match addr {
|
||||||
0xFF04 => self.div = 0,
|
0xFF04 => self.div = 0,
|
||||||
0xFF05 => self.tima = val,
|
0xFF05 => self.tima = val,
|
||||||
0xFF06 => self.tma = val,
|
0xFF06 => self.tma = val,
|
||||||
0xFF07 => self.tac = val,
|
0xFF07 => self.tac = val,
|
||||||
_ => println!("Timer: Write {:02X} to {:04X} unsupported", val, addr),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_byte(&self, addr: u16) -> u8 {
|
pub fn read_byte(&self, addr: u16) -> u8 {
|
||||||
match addr {
|
let r = match addr {
|
||||||
0xFF04 => self.div,
|
0xFF04 => self.div,
|
||||||
0xFF05 => self.tima,
|
0xFF05 => self.tima,
|
||||||
0xFF06 => self.tma,
|
0xFF06 => self.tma,
|
||||||
0xFF07 => self.tac,
|
0xFF07 => self.tac,
|
||||||
_ => {
|
_ => unreachable!(),
|
||||||
println!("Timer: Read from {:04X} unsupported", addr);
|
};
|
||||||
0
|
// println!("Timer RD: {:04X} = {:02X}", addr, r);
|
||||||
},
|
r
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timer_interrupt(&mut self) -> bool {
|
pub fn timer_interrupt(&mut self) -> bool {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user