feat: add more debug tools to gui

This commit is contained in:
Rekai Nyangadzayi Musuka 2021-12-09 05:21:05 -04:00
parent bbcbbd8ce3
commit e72b11f946
7 changed files with 172 additions and 92 deletions

View File

@ -73,15 +73,12 @@ impl Cpu {
/// ///
/// If opcode == 0xCB, then decoding costs 4 cycles. /// If opcode == 0xCB, then decoding costs 4 cycles.
/// Otherwise, decoding is free /// Otherwise, decoding is free
pub(crate) fn decode(&mut self, mut opcode: u8) -> Instruction { pub(crate) fn decode(&mut self, opcode: u8) -> Instruction {
let instr = if opcode == 0xCB { if opcode == 0xCB {
opcode = self.fetch(); Instruction::decode(self.fetch(), true)
Instruction::decode(opcode, true)
} else { } else {
Instruction::decode(opcode, false) Instruction::decode(opcode, false)
}; }
instr.unwrap_or_else(|| panic!("{:#04X} is an invalid instruction", opcode))
} }
/// Execute an [Instruction]. /// Execute an [Instruction].
@ -366,15 +363,12 @@ impl Cpu {
} }
fn _dbg_instr(&self) -> Instruction { fn _dbg_instr(&self) -> Instruction {
let mut byte = self.read_byte(self.reg.pc); let byte = self.read_byte(self.reg.pc);
let instr = if byte == 0xCB { if byte == 0xCB {
byte = self.read_byte(self.reg.pc + 1); Instruction::decode(self.read_byte(self.reg.pc + 1), true)
Instruction::decode(byte, true)
} else { } else {
Instruction::decode(byte, false) Instruction::decode(byte, false)
}; }
instr.unwrap_or_else(|| panic!("{:#04X} is an invalid instruction", byte))
} }
} }

View File

@ -18,7 +18,13 @@ pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines
pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz pub(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz
const DEFAULT_TITLE: &str = "Game Boy Screen"; const DEFAULT_TITLE: &str = "Game Boy Screen";
pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycle { #[inline]
pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) {
run(cpu, gamepad, key, CYCLES_IN_FRAME)
}
#[inline]
pub fn run(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput, cycles: Cycle) {
let mut elapsed = 0; let mut elapsed = 0;
if let Some(event) = gamepad.next_event() { if let Some(event) = gamepad.next_event() {
@ -26,11 +32,9 @@ pub fn run_frame(cpu: &mut Cpu, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycl
} }
crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key); crate::joypad::handle_keyboard_input(&mut cpu.bus.joyp, key);
while elapsed < CYCLES_IN_FRAME { while elapsed < cycles {
elapsed += cpu.step(); elapsed += cpu.step();
} }
elapsed
} }
pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) { pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {

View File

@ -26,6 +26,7 @@ pub struct GuiState {
/// When true, egui winit should exit the application /// When true, egui winit should exit the application
pub quit: bool, pub quit: bool,
pub title: String, pub title: String,
pub mode: EmuMode,
} }
impl GuiState { impl GuiState {
@ -33,10 +34,19 @@ impl GuiState {
Self { Self {
title, title,
quit: Default::default(), quit: Default::default(),
mode: EmuMode::Running,
} }
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EmuMode {
Running,
StepFrame,
Stopped,
Step,
}
/// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit, /// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit,
/// we can use a "default" value. However, in order for this to work the chosen "default" /// we can use a "default" value. However, in order for this to work the chosen "default"
/// value must be an **unused** key, so that it is ignored by the emulator. /// value must be an **unused** key, so that it is ignored by the emulator.
@ -223,7 +233,7 @@ pub fn execute_render_pass(
#[inline] #[inline]
pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) {
use crate::{cpu, instruction}; use crate::{cpu, instruction, ppu};
fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response { fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response {
ui.add(egui::TextEdit::multiline(&mut text).code_editor()) ui.add(egui::TextEdit::multiline(&mut text).code_editor())
@ -247,6 +257,17 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur
selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20)); selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20));
}); });
egui::Window::new("Settings").show(ctx, |ui| {
egui::ComboBox::from_label("Emulation Mode")
.selected_text(format!("{:?}", app.mode))
.show_ui(ui, |ui| {
ui.selectable_value(&mut app.mode, EmuMode::Running, "Running");
ui.selectable_value(&mut app.mode, EmuMode::Stopped, "Stopped");
ui.selectable_value(&mut app.mode, EmuMode::StepFrame, "Step Frame");
ui.selectable_value(&mut app.mode, EmuMode::Step, "Step");
})
});
egui::Window::new("GB Info").show(ctx, |ui| { egui::Window::new("GB Info").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.vertical(|ui| { ui.vertical(|ui| {
@ -262,10 +283,21 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur
}); });
ui.vertical(|ui| { ui.vertical(|ui| {
let ppu = &cpu.bus.ppu;
ui.heading("PPU"); ui.heading("PPU");
ui.monospace("LY: ?"); ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu)));
ui.monospace("SCX: ?"); ui.horizontal(|ui| {
ui.monospace(format!("SCX: {}", ppu::dbg::scx(ppu)));
ui.monospace(format!("SCY: {}", ppu::dbg::scy(ppu)));
});
ui.horizontal(|ui| {
ui.monospace(format!("WX: {}", ppu::dbg::wx(ppu)));
ui.monospace(format!("WY: {}", ppu::dbg::wy(ppu)));
});
ui.monospace(format!("Mode: {:?}", ppu::dbg::mode(ppu)))
}); });
}); });
@ -312,3 +344,12 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: Textur
}) })
}); });
} }
pub mod kbd {
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
pub fn space_released(input: &KeyboardInput) -> bool {
let keycode = input.virtual_keycode;
matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space))
}
}

View File

@ -58,6 +58,7 @@ pub(crate) enum Instruction {
BIT(u8, Register), BIT(u8, Register),
RES(u8, Register), RES(u8, Register),
SET(u8, Register), SET(u8, Register),
Invalid,
} }
impl std::fmt::Debug for Instruction { impl std::fmt::Debug for Instruction {
@ -109,6 +110,7 @@ impl std::fmt::Debug for Instruction {
BIT(b, r) => write!(f, "BIT {}, {:?}", b, r), BIT(b, r) => write!(f, "BIT {}, {:?}", b, r),
RES(b, r) => write!(f, "RES {}, {:?}", b, r), RES(b, r) => write!(f, "RES {}, {:?}", b, r),
SET(b, r) => write!(f, "SET {}, {:?}", b, r), SET(b, r) => write!(f, "SET {}, {:?}", b, r),
Invalid => f.write_str("???"),
} }
} }
} }
@ -1294,6 +1296,7 @@ impl Instruction {
} }
} }
} }
Instruction::Invalid => panic!("Attempted to execute invalid instruction"),
} }
} }
@ -1538,7 +1541,7 @@ impl Instruction {
} }
impl Instruction { impl Instruction {
pub(crate) fn decode(byte: u8, prefixed: bool) -> Option<Self> { pub(crate) fn decode(byte: u8, prefixed: bool) -> Self {
if prefixed { if prefixed {
Self::prefixed(byte) Self::prefixed(byte)
} else { } else {
@ -1546,139 +1549,132 @@ impl Instruction {
} }
} }
fn unprefixed(byte: u8) -> Option<Self> { fn unprefixed(byte: u8) -> Self {
use Instruction::*; use Instruction::*;
match byte { match byte {
// NOP // NOP
0o000 => Some(NOP), 0o000 => NOP,
// LD (u16), SP // LD (u16), SP
0o010 => Some(LD(LDTarget::IndirectImmediateWord, LDSource::SP)), 0o010 => LD(LDTarget::IndirectImmediateWord, LDSource::SP),
// STOP // STOP
0o020 => Some(STOP), 0o020 => STOP,
// JR i8 // JR i8
0o030 => Some(JR(JpCond::Always)), 0o030 => JR(JpCond::Always),
// JR cond i8 // JR cond i8
0o040 | 0o050 | 0o060 | 0o070 => Some(JR(jump_cond((byte >> 3) & 0x03))), 0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)),
// LD r16, u16 // LD r16, u16
0o001 | 0o021 | 0o041 | 0o061 => Some(LD( 0o001 | 0o021 | 0o041 | 0o061 => LD(
LDTarget::Group1(group1((byte >> 4) & 0x03)), LDTarget::Group1(group1((byte >> 4) & 0x03)),
LDSource::ImmediateWord, LDSource::ImmediateWord,
)), ),
// ADD HL, r16 // ADD HL, r16
0o011 | 0o031 | 0o051 | 0o071 => Some(ADD( 0o011 | 0o031 | 0o051 | 0o071 => {
AddTarget::HL, ADD(AddTarget::HL, AddSource::Group1(group1((byte >> 4) & 0x03)))
AddSource::Group1(group1((byte >> 4) & 0x03)), }
)),
// LD (r16), A // LD (r16), A
0o002 | 0o022 | 0o042 | 0o062 => Some(LD( 0o002 | 0o022 | 0o042 | 0o062 => LD(
LDTarget::IndirectGroup2(group2((byte >> 4) & 0x03)), LDTarget::IndirectGroup2(group2((byte >> 4) & 0x03)),
LDSource::A, LDSource::A,
)), ),
// LD A, (r16) // LD A, (r16)
0o012 | 0o032 | 0o052 | 0o072 => Some(LD( 0o012 | 0o032 | 0o052 | 0o072 => LD(
LDTarget::A, LDTarget::A,
LDSource::IndirectGroup2(group2((byte >> 4) & 0x03)), LDSource::IndirectGroup2(group2((byte >> 4) & 0x03)),
)), ),
// INC r16 // INC r16
0o003 | 0o023 | 0o043 | 0o063 => { 0o003 | 0o023 | 0o043 | 0o063 => INC(AllRegisters::Group1(group1((byte >> 4) & 0x03))),
Some(INC(AllRegisters::Group1(group1((byte >> 4) & 0x03))))
}
// DEC r16 // DEC r16
0o013 | 0o033 | 0o053 | 0o073 => { 0o013 | 0o033 | 0o053 | 0o073 => DEC(AllRegisters::Group1(group1((byte >> 4) & 0x03))),
Some(DEC(AllRegisters::Group1(group1((byte >> 4) & 0x03))))
}
// INC r8 // INC r8
0o004 | 0o014 | 0o024 | 0o034 | 0o044 | 0o054 | 0o064 | 0o074 => { 0o004 | 0o014 | 0o024 | 0o034 | 0o044 | 0o054 | 0o064 | 0o074 => {
Some(INC(AllRegisters::Register(register((byte >> 3) & 0x07)))) INC(AllRegisters::Register(register((byte >> 3) & 0x07)))
} }
// DEC r8 // DEC r8
0o005 | 0o015 | 0o025 | 0o035 | 0o045 | 0o055 | 0o065 | 0o075 => { 0o005 | 0o015 | 0o025 | 0o035 | 0o045 | 0o055 | 0o065 | 0o075 => {
Some(DEC(AllRegisters::Register(register((byte >> 3) & 0x07)))) DEC(AllRegisters::Register(register((byte >> 3) & 0x07)))
} }
// LD r8, u8 // LD r8, u8
0o006 | 0o016 | 0o026 | 0o036 | 0o046 | 0o056 | 0o066 | 0o076 => Some(LD( 0o006 | 0o016 | 0o026 | 0o036 | 0o046 | 0o056 | 0o066 | 0o076 => LD(
LDTarget::Register(register((byte >> 3) & 0x07)), LDTarget::Register(register((byte >> 3) & 0x07)),
LDSource::ImmediateByte, LDSource::ImmediateByte,
)), ),
// RLCA, RRCA, RLA, RRA, DAA, CPL, SCF, and CCF // RLCA, RRCA, RLA, RRA, DAA, CPL, SCF, and CCF
0o007 | 0o017 | 0o027 | 0o037 | 0o047 | 0o057 | 0o067 | 0o077 => { 0o007 | 0o017 | 0o027 | 0o037 | 0o047 | 0o057 | 0o067 | 0o077 => {
Some(flag_instr((byte >> 3) & 0x07)) flag_instr((byte >> 3) & 0x07)
} }
// HALT // HALT
0o166 => Some(HALT), 0o166 => HALT,
// LD r8, r8 // LD r8, r8
0o100..=0o177 => Some(LD( 0o100..=0o177 => LD(
LDTarget::Register(register((byte >> 3) & 0x07)), LDTarget::Register(register((byte >> 3) & 0x07)),
LDSource::Register(register(byte & 0x07)), LDSource::Register(register(byte & 0x07)),
)), ),
// ADD, ADC, SUB, SBC, AND, XOR, OR, and CP // ADD, ADC, SUB, SBC, AND, XOR, OR, and CP
0o200..=0o277 => Some(alu_reg_instr((byte >> 3) & 0x07, byte & 0x07)), 0o200..=0o277 => alu_reg_instr((byte >> 3) & 0x07, byte & 0x07),
// RET cond // RET cond
0o300 | 0o310 | 0o320 | 0o330 => Some(RET(jump_cond((byte >> 3) & 0x03))), 0o300 | 0o310 | 0o320 | 0o330 => RET(jump_cond((byte >> 3) & 0x03)),
// LD (0xFF00 + u8), A // LD (0xFF00 + u8), A
0o340 => Some(LD(LDTarget::IoWithImmediateOffset, LDSource::A)), 0o340 => LD(LDTarget::IoWithImmediateOffset, LDSource::A),
// ADD SP, i8 // ADD SP, i8
0o350 => Some(ADD(AddTarget::SP, AddSource::ImmediateSignedByte)), 0o350 => ADD(AddTarget::SP, AddSource::ImmediateSignedByte),
// LD A, (0xFF00 + u8) // LD A, (0xFF00 + u8)
0o360 => Some(LD(LDTarget::A, LDSource::IoWithImmediateOffset)), 0o360 => LD(LDTarget::A, LDSource::IoWithImmediateOffset),
// LD HL, SP + i8 // LD HL, SP + i8
0o370 => Some(LDHL), 0o370 => LDHL,
// POP r16 // POP r16
0o301 | 0o321 | 0o341 | 0o361 => Some(POP(group3((byte >> 4) & 0x03))), 0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)),
// RET // RET
0o311 => Some(RET(JpCond::Always)), 0o311 => RET(JpCond::Always),
// RETI // RETI
0o331 => Some(RETI), 0o331 => RETI,
// JP HL // JP HL
0o351 => Some(JP(JpCond::Always, JpLoc::HL)), 0o351 => JP(JpCond::Always, JpLoc::HL),
// LD SP, HL // LD SP, HL
0o371 => Some(LD(LDTarget::SP, LDSource::HL)), 0o371 => LD(LDTarget::SP, LDSource::HL),
// JP cond u16 // JP cond u16
0o302 | 0o312 | 0o322 | 0o332 => { 0o302 | 0o312 | 0o322 | 0o332 => {
Some(JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord)) JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord)
} }
// LD (0xFF00 + C), A // LD (0xFF00 + C), A
0o342 => Some(LD(LDTarget::IoWithC, LDSource::A)), 0o342 => LD(LDTarget::IoWithC, LDSource::A),
// LD (u16), A // LD (u16), A
0o352 => Some(LD(LDTarget::IndirectImmediateWord, LDSource::A)), 0o352 => LD(LDTarget::IndirectImmediateWord, LDSource::A),
// LD A, (0xFF00 + C) // LD A, (0xFF00 + C)
0o362 => Some(LD(LDTarget::A, LDSource::IoWithC)), 0o362 => LD(LDTarget::A, LDSource::IoWithC),
// LD A, (u16) // LD A, (u16)
0o372 => Some(LD(LDTarget::A, LDSource::IndirectImmediateWord)), 0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord),
// JP u16 // JP u16
0o303 => Some(JP(JpCond::Always, JpLoc::ImmediateWord)), 0o303 => JP(JpCond::Always, JpLoc::ImmediateWord),
// DI // DI
0o363 => Some(DI), 0o363 => DI,
// EI // EI
0o373 => Some(EI), 0o373 => EI,
// CALL cond u16 // CALL cond u16
0o304 | 0o314 | 0o324 | 0o334 => Some(CALL(jump_cond((byte >> 3) & 0x03))), 0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)),
// PUSH r16 // PUSH r16
0o305 | 0o325 | 0o345 | 0o365 => Some(PUSH(group3((byte >> 4) & 0x03))), 0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)),
0o315 => Some(CALL(JpCond::Always)), 0o315 => CALL(JpCond::Always),
0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => { 0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => {
Some(alu_imm_instr((byte >> 3) & 0x07)) alu_imm_instr((byte >> 3) & 0x07)
} }
0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => { 0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000),
Some(RST(byte & 0b00111000)) _ => Invalid, // 0xCB is 0o313
}
_ => None, // 0xCB is 0o313
} }
} }
fn prefixed(byte: u8) -> Option<Self> { fn prefixed(byte: u8) -> Self {
use Instruction::*; use Instruction::*;
match byte { match byte {
// RLC, RRC, RL, RR, SLA, SRA, SWAP and SRL // RLC, RRC, RL, RR, SLA, SRA, SWAP and SRL
0o000..=0o077 => Some(prefix_alu((byte >> 3) & 0x07, byte & 0x07)), 0o000..=0o077 => prefix_alu((byte >> 3) & 0x07, byte & 0x07),
// BIT bit, r8 // BIT bit, r8
0o100..=0o177 => Some(BIT((byte >> 3) & 0x07, register(byte & 0x07))), 0o100..=0o177 => BIT((byte >> 3) & 0x07, register(byte & 0x07)),
// RES bit, r8 // RES bit, r8
0o200..=0o277 => Some(RES((byte >> 3) & 0x07, register(byte & 0x07))), 0o200..=0o277 => RES((byte >> 3) & 0x07, register(byte & 0x07)),
// SET bit, r8 // SET bit, r8
0o300..=0o377 => Some(SET((byte >> 3) & 0x07, register(byte & 0x07))), 0o300..=0o377 => SET((byte >> 3) & 0x07, register(byte & 0x07)),
} }
} }
} }
@ -2163,7 +2159,7 @@ pub(crate) mod dbg {
use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair}; use super::{AllRegisters, BusIo, Cpu, Instruction, RegisterPair};
pub(crate) fn tmp_disasm(cpu: &Cpu, limit: u8) -> String { pub(crate) fn tmp_disasm(cpu: &Cpu, limit: u8) -> String {
let mut asm = String::new(); let mut sm83_asm = String::new();
let mut pc = cpu.register_pair(RegisterPair::PC); let mut pc = cpu.register_pair(RegisterPair::PC);
for _ in 0..limit { for _ in 0..limit {
@ -2179,15 +2175,17 @@ pub(crate) mod dbg {
Instruction::unprefixed(opcode) Instruction::unprefixed(opcode)
}; };
if let Some(instr) = maybe_instr { match maybe_instr {
let instr_asm = format!("{:04X} {:?}\n", pc - 1, instr); Instruction::Invalid => {}
asm.push_str(&instr_asm); instr => {
let asm = format!("{:04X} {:?}\n", pc - 1, instr);
sm83_asm.push_str(&asm);
pc += delta::pc_inc_count(instr) pc += delta::pc_inc_count(instr)
} }
} }
}
asm sm83_asm
} }
fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String { fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String {

View File

@ -1,3 +1,5 @@
#![allow(clippy::derivable_impls)] // Will remove this if bitfield-rs allows default impls
pub use apu::gen::init as spsc_init; pub use apu::gen::init as spsc_init;
pub type Cycle = u64; pub type Cycle = u64;

View File

@ -2,6 +2,7 @@ use std::time::Instant;
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
use egui_wgpu_backend::RenderPass; use egui_wgpu_backend::RenderPass;
use gb::gui::EmuMode;
use gb::{emu, gui}; use gb::{emu, gui};
use gilrs::Gilrs; use gilrs::Gilrs;
use gui::GuiState; use gui::GuiState;
@ -129,7 +130,19 @@ fn main() {
emu::save_and_exit(&cpu, control_flow); emu::save_and_exit(&cpu, control_flow);
} }
emu::run_frame(&mut cpu, &mut gamepad, last_key); match app.mode {
EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key),
EmuMode::StepFrame if gui::kbd::space_released(&last_key) => {
emu::run_frame(&mut cpu, &mut gamepad, last_key)
}
EmuMode::Step if gui::kbd::space_released(&last_key) => {
emu::run(&mut cpu, &mut gamepad, last_key, 4);
}
_ => {}
};
// Input has been consumed, reset it
last_key = gui::unused_key();
window.request_redraw(); window.request_redraw();
} }

View File

@ -910,3 +910,31 @@ struct WindowStatus {
/// drawing from the window tile map is true /// drawing from the window tile map is true
enabled: bool, enabled: bool,
} }
pub(crate) mod dbg {
use super::{Ppu, PpuMode};
pub(crate) fn ly(ppu: &Ppu) -> u8 {
ppu.pos.line_y
}
pub(crate) fn scx(ppu: &Ppu) -> u8 {
ppu.pos.scroll_x
}
pub(crate) fn scy(ppu: &Ppu) -> u8 {
ppu.pos.scroll_y
}
pub(crate) fn mode(ppu: &Ppu) -> PpuMode {
ppu.stat.mode()
}
pub(crate) fn wx(ppu: &Ppu) -> i16 {
ppu.pos.window_x as i16
}
pub(crate) fn wy(ppu: &Ppu) -> i16 {
ppu.pos.window_y as i16
}
}