Compare commits

...

28 Commits

Author SHA1 Message Date
8a352ca807 chore: prevent bitrot 2024-10-13 14:59:24 -05:00
068e4124a6 chore: update dependencies 2024-10-13 14:46:46 -05:00
fedae65cc8 chore: update dependencies 2023-03-17 23:42:51 -05:00
41586d8cc4 chore: update gilrs 2023-02-03 15:20:38 -06:00
79740ac245 chore: update default presenting mode 2022-11-09 08:19:15 -04:00
d6bfde081e feat(ppu): implement double buffering 2022-11-05 02:02:41 -03:00
d2f57ee66b chore: update clap to 4.0 2022-11-05 01:57:44 -03:00
3841d3a98c chore(readme): add build instructions
closes #1
2022-10-26 21:05:00 -03:00
85940c8744 fix: actually paint the gui 2022-10-21 01:17:02 -03:00
c7adf54976 chore: run cargo fmt 2022-10-21 01:13:44 -03:00
f0f9003247 chore: update wgpu 2022-10-21 00:46:14 -03:00
5eba097ad4 chore: update recommended vscode extensions 2022-10-21 00:14:10 -03:00
756c806c2a chore: satisfy clippy 2022-09-12 07:06:09 -03:00
77af5453f0 chore: refactor gui code 2022-09-12 07:00:50 -03:00
41bc4f7afe chore: update dependencies 2022-09-12 06:01:34 -03:00
36e46d3780 chore: update dependencies 2022-04-26 11:45:57 -05:00
926c66746d chore: update some dependencies 2022-04-07 15:30:46 -03:00
cf2357f917 feat: add disassembler that is aware of immediate values 2021-12-16 12:16:58 -04:00
3fb7edd333 chore: add some missing comments to instruction decoding 2021-12-16 12:16:36 -04:00
3ab512d663 fix(ppu): implement proper PPU reset behaviour 2021-12-09 10:46:27 -04:00
61156ca8a8 fix(cart): rename enum variant and enable MBC3 w/ battery 2021-12-09 09:30:47 -04:00
2c411d5792 chore(cart): add test for all-whitespace title 2021-12-09 07:27:52 -04:00
4c657c9832 chore: update dependencies 2021-12-09 06:23:32 -04:00
e72b11f946 feat: add more debug tools to gui 2021-12-09 05:21:05 -04:00
bbcbbd8ce3 chore: fix dependency conflict 2021-12-06 13:36:23 -04:00
c4226e6e76 Merge branch 'main' of ssh://git.musuka.dev:2222/paoda/gb into main 2021-12-06 13:34:14 -04:00
409314a4e5 fix(dbg): don't crash when attempting to read data as executable code 2021-12-06 13:33:22 -04:00
952bf68bd0 chore: add extension to recommended 2021-12-06 13:24:05 -04:00
14 changed files with 2254 additions and 1400 deletions

View File

@@ -1,8 +1,9 @@
{ {
"recommendations": [ "recommendations": [
"matklad.rust-analyzer", "rust-lang.rust-analyzer",
"tamasfe.even-better-toml", "tamasfe.even-better-toml",
"serayuzgur.crates", "serayuzgur.crates",
"vadimcn.vscode-lldb", "vadimcn.vscode-lldb",
"donaldhays.rgbds-z80"
] ]
} }

2533
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,21 +8,21 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
bitfield = "0.13" bitfield = "0.17"
clap = "2.33" clap = { version = "4.0", features = ["cargo"] }
gilrs = "0.8" gilrs = "0.11"
winit = "0.26" winit = "0.28"
egui = "0.15" egui = "0.21"
wgpu = "0.11" egui_wgpu_backend = "0.22"
egui_wgpu_backend = "0.14" egui_winit_platform = "0.18"
egui_winit_platform = "0.11" pollster = "0.3"
pollster = "0.2" rodio = "0.19"
rodio = "0.14" rtrb = "0.3"
rtrb = "0.2"
directories-next = "2.0" directories-next = "2.0"
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] }
thiserror = "1.0" thiserror = "1.0"
once_cell = "1.10"
[profile.release] [profile.release]
debug = true debug = true

View File

@@ -1,10 +1,9 @@
# Rekai's Gameboy Emulator # Rekai's Gameboy Emulator
[![Build Status](https://ci.paoda.moe/api/badges/paoda/gb/status.svg)](https://ci.paoda.moe/paoda/gb)
### Status ### Status
* From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/) * From [Blargg Test ROMs](https://github.com/L-P/blargg-test-roms/)
* [x] cpu_instrs * [x] cpu_instrs
* [ ] instr_timing (kind of) * [x] instr_timing
* [x] mem_timing * [x] mem_timing
* [x] mem_timing-2 * [x] mem_timing-2
* [ ] dmg_sound (partial) * [ ] dmg_sound (partial)
@@ -21,9 +20,15 @@
Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games. Supports: ROM-only, MBC1, MBC2, MBC3 and MBC5 games.
### Compiling
This project was last successfully built on [Rust 1.64.0](https://github.com/rust-lang/rust/tree/1.64.0)
1. `git clone https://github.com/paoda/gb`
2. `cd gb`
3. `cargo run --release`
### Controls ### Controls
Controls are defined [here](https://git.musuka.dev/paoda/gb/src/branch/main/src/joypad.rs#L114) Controls are defined [here](https://github.com/paoda/gb/blob/85940c874460b9cb31bf9b8211bf7dda596d114a/src/joypad.rs#L114)
Key | Button Key | Button
--- | --- --- | ---

View File

@@ -169,7 +169,7 @@ pub(super) mod ch4 {
match byte & 0x01 { match byte & 0x01 {
0b00 => Self::Short, 0b00 => Self::Short,
0b01 => Self::Long, 0b01 => Self::Long,
_ => unreachable!("{:#04X} is not a valid value for CounterWidth"), _ => unreachable!("{:#04X} is not a valid value for CounterWidth", byte),
} }
} }
} }

View File

@@ -61,8 +61,9 @@ impl Cartridge {
MbcKind::Mbc2(hw) => Box::new(Mbc2::new(hw, rom_cap)), MbcKind::Mbc2(hw) => Box::new(Mbc2::new(hw, rom_cap)),
MbcKind::Mbc3(hw @ Mbc3Hardware::Rtc) => Box::new(Mbc3::new(hw, ram_cap)), MbcKind::Mbc3(hw @ Mbc3Hardware::Rtc) => Box::new(Mbc3::new(hw, ram_cap)),
MbcKind::Mbc3(hw @ Mbc3Hardware::RtcBatteryRam) => Box::new(Mbc3::new(hw, ram_cap)), MbcKind::Mbc3(hw @ Mbc3Hardware::RtcBatteryRam) => Box::new(Mbc3::new(hw, ram_cap)),
MbcKind::Mbc4(hw @ Mbc5Hardware::None) => Box::new(Mbc5::new(hw, ram_cap, rom_cap)), MbcKind::Mbc3(hw @ Mbc3Hardware::BatteryRam) => Box::new(Mbc3::new(hw, ram_cap)),
MbcKind::Mbc4(hw @ Mbc5Hardware::BatteryRam) => { MbcKind::Mbc5(hw @ Mbc5Hardware::None) => Box::new(Mbc5::new(hw, ram_cap, rom_cap)),
MbcKind::Mbc5(hw @ Mbc5Hardware::BatteryRam) => {
Box::new(Mbc5::new(hw, ram_cap, rom_cap)) Box::new(Mbc5::new(hw, ram_cap, rom_cap))
} }
kind => todo!("ROMS with {:?} are currently unsupported", kind), kind => todo!("ROMS with {:?} are currently unsupported", kind),
@@ -70,21 +71,23 @@ impl Cartridge {
} }
fn detect_title(title_mem: &[u8; ROM_TITLE_MAX_SIZE]) -> Option<String> { fn detect_title(title_mem: &[u8; ROM_TITLE_MAX_SIZE]) -> Option<String> {
use std::str::from_utf8;
const ALT_TITLE_LEN: usize = ROM_MANUFACTURER_START - ROM_TITLE_START; const ALT_TITLE_LEN: usize = ROM_MANUFACTURER_START - ROM_TITLE_START;
// byte slice we have here is purposely not null terminated // ascii byte slie does not have a null-terminator
let ascii = match title_mem.iter().position(|b| *b == 0x00) { let ascii = match title_mem.iter().position(|b| *b == 0x00) {
Some(end) => &title_mem[0..end], Some(end) => &title_mem[..end],
None => &title_mem[0..ROM_TITLE_MAX_SIZE], None => &title_mem[..ROM_TITLE_MAX_SIZE],
}; };
match std::str::from_utf8(ascii).ok() { match from_utf8(ascii).map(str::trim).ok() {
None => match std::str::from_utf8(&title_mem[0..ALT_TITLE_LEN]).ok() { None => match from_utf8(&title_mem[..ALT_TITLE_LEN]).map(str::trim).ok() {
Some("") | None => None, Some("") | None => None,
Some(title) => Some(String::from(title.trim())), Some(title) => Some(String::from(title)),
}, },
Some("") => None, Some("") => None,
Some(title) => Some(String::from(title.trim())), Some(title) => Some(String::from(title)),
} }
} }
@@ -105,12 +108,12 @@ impl Cartridge {
0x11 => Mbc3(Mbc3Hardware::None), 0x11 => Mbc3(Mbc3Hardware::None),
0x12 => Mbc3(Mbc3Hardware::Ram), 0x12 => Mbc3(Mbc3Hardware::Ram),
0x13 => Mbc3(Mbc3Hardware::BatteryRam), 0x13 => Mbc3(Mbc3Hardware::BatteryRam),
0x19 => Mbc4(Mbc5Hardware::None), 0x19 => Mbc5(Mbc5Hardware::None),
0x1A => Mbc4(Mbc5Hardware::Ram), 0x1A => Mbc5(Mbc5Hardware::Ram),
0x1B => Mbc4(Mbc5Hardware::BatteryRam), 0x1B => Mbc5(Mbc5Hardware::BatteryRam),
0x1C => Mbc4(Mbc5Hardware::Rumble), 0x1C => Mbc5(Mbc5Hardware::Rumble),
0x1D => Mbc4(Mbc5Hardware::RumbleRam), 0x1D => Mbc5(Mbc5Hardware::RumbleRam),
0x1E => Mbc4(Mbc5Hardware::RumbleBatteryRam), 0x1E => Mbc5(Mbc5Hardware::RumbleBatteryRam),
id => unimplemented!("MBC with code {:#04X} is unsupported", id), id => unimplemented!("MBC with code {:#04X} is unsupported", id),
} }
} }
@@ -746,7 +749,7 @@ enum MbcKind {
Mbc1(Mbc1Hardware), Mbc1(Mbc1Hardware),
Mbc2(Mbc2Hardware), Mbc2(Mbc2Hardware),
Mbc3(Mbc3Hardware), Mbc3(Mbc3Hardware),
Mbc4(Mbc5Hardware), Mbc5(Mbc5Hardware),
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@@ -950,4 +953,14 @@ mod tests {
Cartridge::detect_title(&title) Cartridge::detect_title(&title)
); );
} }
#[test]
fn all_whitespace_title() {
let title = [
0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00,
];
assert_eq!(None, Cartridge::detect_title(&title));
}
} }

View File

@@ -518,9 +518,6 @@ pub(crate) mod dbg {
} }
pub(crate) fn ime(cpu: &Cpu) -> bool { pub(crate) fn ime(cpu: &Cpu) -> bool {
match cpu.ime { matches!(cpu.ime, ImeState::Enabled)
ImeState::Enabled => true,
_ => false,
}
} }
} }

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) {
@@ -40,7 +44,8 @@ pub fn save_and_exit(cpu: &Cpu, control_flow: &mut ControlFlow) {
#[inline] #[inline]
pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] { pub fn pixel_buf(cpu: &Cpu) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
cpu.bus.ppu.frame_buf.as_ref() use crate::ppu::Device;
cpu.bus.ppu.frame_buf.get(Device::Host)
} }
pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> { pub fn from_boot_rom<P: AsRef<Path>>(path: P) -> std::io::Result<Cpu> {
@@ -60,8 +65,7 @@ pub fn rom_title(cpu: &Cpu) -> &str {
cpu.bus cpu.bus
.cart .cart
.as_ref() .as_ref()
.map(|c| c.title.as_deref()) .and_then(|c| c.title.as_deref())
.flatten()
.unwrap_or(DEFAULT_TITLE) .unwrap_or(DEFAULT_TITLE)
} }

View File

@@ -1,18 +1,20 @@
use egui::{ClippedMesh, CtxRef, TextureId}; use egui::{Context, TextureId};
use egui_wgpu_backend::{BackendError, RenderPass, ScreenDescriptor}; use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor};
use egui_winit_platform::Platform; use egui_winit_platform::Platform;
use wgpu::{ use wgpu::{
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError, Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
Surface, SurfaceConfiguration, SurfaceTexture, Texture, TextureFormat, TextureUsages, Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView,
TextureView, TextureViewDescriptor,
}; };
use winit::dpi::PhysicalSize;
use winit::error::OsError; use winit::error::OsError;
use winit::event::{ElementState, KeyboardInput}; use winit::event::{ElementState, Event, KeyboardInput};
use winit::event_loop::EventLoop; use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::Window; use winit::window::Window;
use crate::cpu::Cpu; use crate::cpu::Cpu;
use crate::{GB_HEIGHT, GB_WIDTH}; use crate::{emu, GB_HEIGHT, GB_WIDTH};
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720); const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
const FILTER_MODE: FilterMode = FilterMode::Nearest; const FILTER_MODE: FilterMode = FilterMode::Nearest;
@@ -20,21 +22,287 @@ const WINDOW_TITLE: &str = "DMG-01 Emulator";
const SCALE: f32 = 3.0; const SCALE: f32 = 3.0;
/// Holds GUI State pub struct Gui {
#[derive(Debug, Clone)] pub should_quit: bool,
pub struct GuiState {
/// When true, egui winit should exit the application
pub quit: bool,
pub title: String, pub title: String,
pub mode: EmuMode,
window: Window,
platform: Platform,
surface: Surface,
device: Device,
queue: Queue,
surface_config: SurfaceConfiguration,
render_pass: RenderPass,
texture: Texture,
texture_size: Extent3d,
texture_id: TextureId,
} }
impl GuiState { impl Gui {
pub fn new(title: String) -> Self { pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> Self {
use wgpu::InstanceDescriptor;
let window = build_window(event_loop).expect("build window");
let instance = Instance::new(InstanceDescriptor::default());
let surface = unsafe {
instance
.create_surface(&window)
.expect("create wgpu instance surface")
};
let adapter = request_adapter(&instance, &surface).expect("request adaptor");
let (device, queue) = request_device(&adapter).expect("request device");
let capabilities = surface.get_capabilities(&adapter);
let surface_config = surface_config(&window, capabilities);
surface.configure(&device, &surface_config);
let platform = platform(&window);
let mut render_pass = RenderPass::new(&device, surface_config.format, 1);
let texture_size = texture_size();
let texture = create_texture(&device, texture_size);
write_to_texture(&queue, &texture, emu::pixel_buf(cpu), texture_size);
let view = texture.create_view(&TextureViewDescriptor::default());
let texture_id = expose_texture_to_egui(&mut render_pass, &device, &view);
Self { Self {
should_quit: Default::default(),
title, title,
quit: Default::default(), mode: EmuMode::Running,
window,
platform,
surface,
device,
queue,
surface_config,
render_pass,
texture,
texture_size,
texture_id,
} }
} }
pub fn maybe_quit(&self, cpu: &Cpu, control_flow: &mut ControlFlow) {
if self.should_quit {
emu::save_and_exit(cpu, control_flow);
}
}
pub fn request_redraw(&self) {
self.window.request_redraw();
}
pub fn handle_event<T>(&mut self, event: &Event<T>) {
self.platform.handle_event(event);
}
pub fn update_time(&mut self, elapsed_seconds: f64) {
self.platform.update_time(elapsed_seconds);
}
pub fn resize(&mut self, size: PhysicalSize<u32>) {
// See: https://github.com/rust-windowing/winit/issues/208
if size.width > 0 && size.height > 0 {
self.surface_config.width = size.width;
self.surface_config.height = size.height;
self.surface.configure(&self.device, &self.surface_config);
}
}
pub fn paint(&mut self, cpu: &Cpu) {
use wgpu::{Color, SurfaceError};
let data = emu::pixel_buf(cpu);
write_to_texture(&self.queue, &self.texture, data, self.texture_size);
let output_frame = match self.surface.get_current_texture() {
Ok(frame) => frame,
Err(SurfaceError::Outdated) => return, // Occurs on minimization on Windows
Err(e) => {
eprintln!("Dropped frame with error: {}", e);
return;
}
};
let output_view = output_frame
.texture
.create_view(&TextureViewDescriptor::default());
// Begin to draw Egui components
self.platform.begin_frame();
self.draw_egui(cpu, &self.platform.context(), self.texture_id);
// End the UI frame. We could now handle the output and draw the UI with the backend.
let full_output = self.platform.end_frame(Some(&self.window));
let paint_jobs = self.platform.context().tessellate(full_output.shapes);
let mut encoder = create_command_encoder(&self.device);
let screen_descriptor = create_screen_descriptor(&self.window, &self.surface_config);
let tdelta = full_output.textures_delta;
// Upload all resources for the GPU.
self.render_pass
.add_textures(&self.device, &self.queue, &tdelta)
.expect("add texture ok");
self.render_pass
.update_buffers(&self.device, &self.queue, &paint_jobs, &screen_descriptor);
// Record all render passes.
self.render_pass
.execute(
&mut encoder,
&output_view,
&paint_jobs,
&screen_descriptor,
Some(Color::BLACK),
)
.expect("execute render pass");
// Submit the commands.
self.queue.submit(std::iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
self.render_pass
.remove_textures(tdelta)
.expect("remove texture delta");
}
#[inline]
pub fn draw_egui(&mut self, cpu: &Cpu, ctx: &Context, texture_id: TextureId) {
use crate::{cpu, instruction, ppu};
fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response {
ui.add(egui::TextEdit::multiline(&mut text).code_editor())
}
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.menu_button("File", |ui| {
if ui.button("Quit").clicked() {
self.should_quit = true;
}
});
egui::Window::new(&self.title).show(ctx, |ui| {
ui.image(
texture_id,
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
);
});
egui::Window::new("Disassembly").show(ctx, |ui| {
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!("{:?}", self.mode))
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.mode, EmuMode::Running, "Running");
ui.selectable_value(&mut self.mode, EmuMode::Stopped, "Stopped");
ui.selectable_value(&mut self.mode, EmuMode::StepFrame, "Step Frame");
ui.selectable_value(&mut self.mode, EmuMode::Step, "Step");
})
});
egui::Window::new("GB Info").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.heading("CPU");
ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu)));
ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu)));
ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu)));
ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu)));
ui.add_space(10.0);
ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu)));
ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu)));
});
ui.vertical(|ui| {
let ppu = &cpu.bus.ppu;
ui.heading("PPU");
ui.monospace(format!("LY: {}", ppu::dbg::ly(ppu)));
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),
ppu::dbg::dot(ppu)
))
});
});
ui.add_space(10.0);
let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME");
ui.horizontal(|ui| {
let irq = cpu.int_request();
ui.label("IRQ:");
let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad");
});
ui.horizontal(|ui| {
let ie = cpu.int_enable();
// TODO: Reimplement this
// let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R');
// let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E');
// let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q');
ui.label("IE:");
// ui.add_space(q_len - (e_len - r_len));
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad");
});
ui.add_space(10.0);
ui.horizontal(|ui| {
let flags = cpu::dbg::flags(cpu);
let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero");
let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative");
let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry");
let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry");
})
})
});
}
}
#[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,
@@ -51,8 +319,7 @@ pub fn unused_key() -> KeyboardInput {
} }
} }
pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> { fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
use winit::dpi::PhysicalSize;
use winit::window::WindowBuilder; use winit::window::WindowBuilder;
WindowBuilder::new() WindowBuilder::new()
@@ -67,51 +334,45 @@ pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
.build(event_loop) .build(event_loop)
} }
pub fn create_surface(window: &Window) -> (Instance, Surface) { fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
use wgpu::Backends;
let instance = Instance::new(Backends::PRIMARY);
let surface = unsafe { instance.create_surface(window) };
(instance, surface)
}
pub fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
use wgpu::{PowerPreference, RequestAdapterOptions}; use wgpu::{PowerPreference, RequestAdapterOptions};
pollster::block_on(instance.request_adapter(&RequestAdapterOptions { pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
power_preference: PowerPreference::HighPerformance, power_preference: PowerPreference::HighPerformance,
force_fallback_adapter: false, // TODO: What do I want to do with this?
compatible_surface: Some(surface), compatible_surface: Some(surface),
force_fallback_adapter: false, // TODO: What do I want to do with this?
})) }))
} }
pub fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> { fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
use wgpu::{DeviceDescriptor, Features, Limits}; use wgpu::{DeviceDescriptor, Features, Limits};
pollster::block_on(adapter.request_device( pollster::block_on(adapter.request_device(
&DeviceDescriptor { &DeviceDescriptor {
label: None,
features: Features::default(), features: Features::default(),
limits: Limits::default(), limits: Limits::default(),
label: None,
}, },
None, None,
)) ))
} }
pub fn surface_config(window: &Window, format: TextureFormat) -> SurfaceConfiguration { fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration {
use wgpu::PresentMode; use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages};
let size = window.inner_size(); let size = window.inner_size();
SurfaceConfiguration { SurfaceConfiguration {
usage: TextureUsages::RENDER_ATTACHMENT, usage: TextureUsages::RENDER_ATTACHMENT,
format, format: capabilities.formats[0],
width: size.width as u32, width: size.width as u32,
height: size.height as u32, height: size.height as u32,
present_mode: PresentMode::Mailbox, present_mode: PresentMode::Fifo,
alpha_mode: capabilities.alpha_modes[0],
view_formats: vec![TextureFormat::Rgba8UnormSrgb],
} }
} }
pub fn platform_desc(window: &Window) -> Platform { fn platform(window: &Window) -> Platform {
use egui::FontDefinitions; use egui::FontDefinitions;
use egui_winit_platform::PlatformDescriptor; use egui_winit_platform::PlatformDescriptor;
@@ -125,7 +386,7 @@ pub fn platform_desc(window: &Window) -> Platform {
}) })
} }
pub fn texture_size() -> Extent3d { fn texture_size() -> Extent3d {
Extent3d { Extent3d {
width: GB_WIDTH as u32, width: GB_WIDTH as u32,
height: GB_HEIGHT as u32, height: GB_HEIGHT as u32,
@@ -133,22 +394,23 @@ pub fn texture_size() -> Extent3d {
} }
} }
pub fn create_texture(device: &Device, size: Extent3d) -> Texture { fn create_texture(device: &Device, size: Extent3d) -> Texture {
use wgpu::{TextureDescriptor, TextureDimension}; use wgpu::{TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
device.create_texture(&TextureDescriptor { device.create_texture(&TextureDescriptor {
size, size,
mip_level_count: 1, mip_level_count: 1,
sample_count: 1, sample_count: 1,
dimension: TextureDimension::D2, dimension: TextureDimension::D2,
format: TextureFormat::Rgba8UnormSrgb, format: TextureFormat::Bgra8Unorm,
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING, usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
label: Some("gb_pixel_buffer"), label: Some("gb_pixel_buffer"),
view_formats: &[TextureFormat::Bgra8Unorm],
}) })
} }
#[inline] #[inline]
pub fn write_to_texture( fn write_to_texture(
queue: &Queue, queue: &Queue,
texture: &Texture, texture: &Texture,
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT], data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
@@ -174,23 +436,16 @@ pub fn write_to_texture(
); );
} }
pub fn expose_texture_to_egui( fn expose_texture_to_egui(
render_pass: &mut RenderPass, render_pass: &mut RenderPass,
device: &Device, device: &Device,
texture: &Texture, view: &TextureView,
) -> TextureId { ) -> TextureId {
render_pass.egui_texture_from_wgpu_texture(device, texture, FILTER_MODE) render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE)
} }
#[inline] #[inline]
pub fn create_view(frame: &SurfaceTexture) -> TextureView { fn create_command_encoder(device: &Device) -> CommandEncoder {
use wgpu::TextureViewDescriptor;
frame.texture.create_view(&TextureViewDescriptor::default())
}
#[inline]
pub fn create_command_encoder(device: &Device) -> CommandEncoder {
use wgpu::CommandEncoderDescriptor; use wgpu::CommandEncoderDescriptor;
device.create_command_encoder(&CommandEncoderDescriptor { device.create_command_encoder(&CommandEncoderDescriptor {
@@ -199,10 +454,7 @@ pub fn create_command_encoder(device: &Device) -> CommandEncoder {
} }
#[inline] #[inline]
pub fn create_screen_descriptor( fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor {
window: &Window,
config: &SurfaceConfiguration,
) -> ScreenDescriptor {
ScreenDescriptor { ScreenDescriptor {
physical_width: config.width, physical_width: config.width,
physical_height: config.height, physical_height: config.height,
@@ -210,105 +462,11 @@ pub fn create_screen_descriptor(
} }
} }
#[inline] pub mod kbd {
pub fn execute_render_pass( use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
render_pass: &mut RenderPass,
encoder: &mut CommandEncoder,
view: &TextureView,
jobs: Vec<ClippedMesh>,
descriptor: &ScreenDescriptor,
) -> Result<(), BackendError> {
render_pass.execute(encoder, view, &jobs, descriptor, Some(wgpu::Color::BLACK))
}
#[inline] pub fn space_released(input: &KeyboardInput) -> bool {
pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) { let keycode = input.virtual_keycode;
use crate::{cpu, instruction}; matches!(input.state, ElementState::Released if keycode == Some(VirtualKeyCode::Space))
fn selectable_text(ui: &mut egui::Ui, mut text: &str) -> egui::Response {
ui.add(egui::TextEdit::multiline(&mut text).code_editor())
} }
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
egui::menu::menu(ui, "File", |ui| {
if ui.button("Quit").clicked() {
app.quit = true;
}
});
egui::Window::new(&app.title).show(ctx, |ui| {
ui.image(
texture_id,
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
);
});
egui::Window::new("Disassembly").show(ctx, |ui| {
selectable_text(ui, &instruction::dbg::tmp_disasm(cpu, 20));
});
egui::Window::new("GB Info").show(ctx, |ui| {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.heading("CPU");
ui.monospace(format!("AF: {:#06X}", cpu::dbg::af(cpu)));
ui.monospace(format!("BC: {:#06X}", cpu::dbg::bc(cpu)));
ui.monospace(format!("DE: {:#06X}", cpu::dbg::de(cpu)));
ui.monospace(format!("HL: {:#06X}", cpu::dbg::hl(cpu)));
ui.add_space(10.0);
ui.monospace(format!("SP: {:#06X}", cpu::dbg::sp(cpu)));
ui.monospace(format!("PC: {:#06X}", cpu::dbg::pc(cpu)));
});
ui.vertical(|ui| {
ui.heading("PPU");
ui.monospace("LY: ?");
ui.monospace("SCX: ?");
});
});
ui.add_space(10.0);
let _ = ui.selectable_label(cpu::dbg::ime(cpu), "IME");
ui.horizontal(|ui| {
let irq = cpu.int_request();
ui.label("IRQ:");
let _ = ui.selectable_label(irq & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(irq >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(irq >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(irq >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(irq >> 4 & 0x01 == 0x01, "Joypad");
});
ui.horizontal(|ui| {
let ie = cpu.int_enable();
let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R');
let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E');
let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q');
ui.label("IE:");
ui.add_space(q_len - (e_len - r_len));
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
let _ = ui.selectable_label(ie >> 3 & 0x01 == 0x01, "Serial");
let _ = ui.selectable_label(ie >> 4 & 0x01 == 0x01, "Joypad");
});
ui.add_space(10.0);
ui.horizontal(|ui| {
let flags = cpu::dbg::flags(cpu);
let _ = ui.selectable_label((flags >> 7 & 0x01) == 0x01, "Zero");
let _ = ui.selectable_label((flags >> 6 & 0x01) == 0x01, "Negative");
let _ = ui.selectable_label((flags >> 5 & 0x01) == 0x01, "Half-Carry");
let _ = ui.selectable_label((flags >> 4 & 0x01) == 0x01, "Carry");
})
})
});
} }

View File

@@ -1,6 +1,6 @@
use self::add::{Source as AddSource, Target as AddTarget}; use self::add::{Source as AddSource, Target as AddTarget};
use self::alu::Source as AluSource; use self::alu::Source as AluSource;
use self::jump::{JumpCondition, JumpLocation}; use self::jump::{JpCond, JpLoc};
use self::load::{Source as LDSource, Target as LDTarget}; use self::load::{Source as LDSource, Target as LDTarget};
use self::table::{ use self::table::{
alu_imm_instr, alu_reg_instr, flag_instr, group1, group2, group3, jump_cond, prefix_alu, alu_imm_instr, alu_reg_instr, flag_instr, group1, group2, group3, jump_cond, prefix_alu,
@@ -16,7 +16,7 @@ use crate::Cycle;
pub(crate) enum Instruction { pub(crate) enum Instruction {
NOP, NOP,
STOP, STOP,
JR(JumpCondition), JR(JpCond),
LD(LDTarget, LDSource), LD(LDTarget, LDSource),
ADD(AddTarget, AddSource), ADD(AddTarget, AddSource),
LDHL, LDHL,
@@ -38,13 +38,13 @@ pub(crate) enum Instruction {
XOR(AluSource), XOR(AluSource),
OR(AluSource), OR(AluSource),
CP(AluSource), CP(AluSource),
RET(JumpCondition), RET(JpCond),
POP(Group3RegisterPair), POP(Group3RegisterPair),
RETI, RETI,
JP(JumpCondition, JumpLocation), JP(JpCond, JpLoc),
DI, DI,
EI, EI,
CALL(JumpCondition), CALL(JpCond),
PUSH(Group3RegisterPair), PUSH(Group3RegisterPair),
RST(u8), RST(u8),
RLC(Register), RLC(Register),
@@ -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("???"),
} }
} }
} }
@@ -293,7 +295,7 @@ impl Instruction {
let addr = pc.wrapping_add(byte as u16); let addr = pc.wrapping_add(byte as u16);
match cond { match cond {
JumpCondition::NotZero => { JpCond::NotZero => {
if !flags.z() { if !flags.z() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
12 12
@@ -301,7 +303,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Zero => { JpCond::Zero => {
if flags.z() { if flags.z() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
12 12
@@ -309,7 +311,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::NotCarry => { JpCond::NotCarry => {
if !flags.c() { if !flags.c() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
12 12
@@ -317,7 +319,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Carry => { JpCond::Carry => {
if flags.c() { if flags.c() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
12 12
@@ -325,7 +327,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Always => { JpCond::Always => {
Self::jump(cpu, addr); Self::jump(cpu, addr);
12 12
} }
@@ -837,7 +839,7 @@ impl Instruction {
let flags: Flags = *cpu.flags(); let flags: Flags = *cpu.flags();
match cond { match cond {
JumpCondition::NotZero => { JpCond::NotZero => {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
if !flags.z() { if !flags.z() {
@@ -848,7 +850,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Zero => { JpCond::Zero => {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
if flags.z() { if flags.z() {
@@ -859,7 +861,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::NotCarry => { JpCond::NotCarry => {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
if !flags.c() { if !flags.c() {
@@ -870,7 +872,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Carry => { JpCond::Carry => {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
if flags.c() { if flags.c() {
@@ -881,7 +883,7 @@ impl Instruction {
8 8
} }
} }
JumpCondition::Always => { JpCond::Always => {
let addr = Self::pop(cpu); let addr = Self::pop(cpu);
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
@@ -908,13 +910,13 @@ impl Instruction {
16 16
} }
Instruction::JP(cond, location) => match location { Instruction::JP(cond, location) => match location {
JumpLocation::HL => { JpLoc::HL => {
// JP HL | Store HL in program counter // JP HL | Store HL in program counter
let right = cpu.register_pair(RegisterPair::HL); let right = cpu.register_pair(RegisterPair::HL);
cpu.set_register_pair(RegisterPair::PC, right); cpu.set_register_pair(RegisterPair::PC, right);
4 4
} }
JumpLocation::ImmediateWord => { JpLoc::ImmediateWord => {
// JP cond u16 | Store u16 in program counter if condition is true // JP cond u16 | Store u16 in program counter if condition is true
// JP u16 | Store u16 in program counter // JP u16 | Store u16 in program counter
let flags: Flags = *cpu.flags(); let flags: Flags = *cpu.flags();
@@ -922,7 +924,7 @@ impl Instruction {
let addr = Self::imm_word(cpu); let addr = Self::imm_word(cpu);
match cond { match cond {
JumpCondition::NotZero => { JpCond::NotZero => {
if !flags.z() { if !flags.z() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
@@ -930,7 +932,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Zero => { JpCond::Zero => {
if flags.z() { if flags.z() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
@@ -938,7 +940,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::NotCarry => { JpCond::NotCarry => {
if !flags.c() { if !flags.c() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
@@ -946,7 +948,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Carry => { JpCond::Carry => {
if flags.c() { if flags.c() {
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
@@ -954,7 +956,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Always => { JpCond::Always => {
Self::jump(cpu, addr); Self::jump(cpu, addr);
16 16
} }
@@ -980,7 +982,7 @@ impl Instruction {
let return_addr = cpu.register_pair(RegisterPair::PC); let return_addr = cpu.register_pair(RegisterPair::PC);
match cond { match cond {
JumpCondition::NotZero => { JpCond::NotZero => {
if !flags.z() { if !flags.z() {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
Self::push(cpu, return_addr); Self::push(cpu, return_addr);
@@ -990,7 +992,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Zero => { JpCond::Zero => {
if flags.z() { if flags.z() {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
Self::push(cpu, return_addr); Self::push(cpu, return_addr);
@@ -1000,7 +1002,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::NotCarry => { JpCond::NotCarry => {
if !flags.c() { if !flags.c() {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
Self::push(cpu, return_addr); Self::push(cpu, return_addr);
@@ -1010,7 +1012,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Carry => { JpCond::Carry => {
if flags.c() { if flags.c() {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
Self::push(cpu, return_addr); Self::push(cpu, return_addr);
@@ -1020,7 +1022,7 @@ impl Instruction {
12 12
} }
} }
JumpCondition::Always => { JpCond::Always => {
cpu.bus.clock(); // internal branch decision cpu.bus.clock(); // internal branch decision
Self::push(cpu, return_addr); Self::push(cpu, return_addr);
cpu.set_register_pair(RegisterPair::PC, addr); cpu.set_register_pair(RegisterPair::PC, addr);
@@ -1294,6 +1296,7 @@ impl Instruction {
} }
} }
} }
Instruction::Invalid => panic!("Attempted to execute invalid instruction"),
} }
} }
@@ -1557,7 +1560,7 @@ impl Instruction {
// STOP // STOP
0o020 => STOP, 0o020 => STOP,
// JR i8 // JR i8
0o030 => JR(JumpCondition::Always), 0o030 => JR(JpCond::Always),
// JR cond i8 // JR cond i8
0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)), 0o040 | 0o050 | 0o060 | 0o070 => JR(jump_cond((byte >> 3) & 0x03)),
// LD r16, u16 // LD r16, u16
@@ -1622,16 +1625,16 @@ impl Instruction {
// POP r16 // POP r16
0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)), 0o301 | 0o321 | 0o341 | 0o361 => POP(group3((byte >> 4) & 0x03)),
// RET // RET
0o311 => RET(JumpCondition::Always), 0o311 => RET(JpCond::Always),
// RETI // RETI
0o331 => RETI, 0o331 => RETI,
// JP HL // JP HL
0o351 => JP(JumpCondition::Always, JumpLocation::HL), 0o351 => JP(JpCond::Always, JpLoc::HL),
// LD SP, HL // LD SP, HL
0o371 => 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 => {
JP(jump_cond((byte >> 3) & 0x03), JumpLocation::ImmediateWord) JP(jump_cond((byte >> 3) & 0x03), JpLoc::ImmediateWord)
} }
// LD (0xFF00 + C), A // LD (0xFF00 + C), A
0o342 => LD(LDTarget::IoWithC, LDSource::A), 0o342 => LD(LDTarget::IoWithC, LDSource::A),
@@ -1642,9 +1645,7 @@ impl Instruction {
// LD A, (u16) // LD A, (u16)
0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord), 0o372 => LD(LDTarget::A, LDSource::IndirectImmediateWord),
// JP u16 // JP u16
0o303 => JP(JumpCondition::Always, JumpLocation::ImmediateWord), 0o303 => JP(JpCond::Always, JpLoc::ImmediateWord),
// 0xCB Prefix
0o313 => unreachable!("{:#04X} should be handled by the prefixed decoder", byte),
// DI // DI
0o363 => DI, 0o363 => DI,
// EI // EI
@@ -1653,12 +1654,14 @@ impl Instruction {
0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)), 0o304 | 0o314 | 0o324 | 0o334 => CALL(jump_cond((byte >> 3) & 0x03)),
// PUSH r16 // PUSH r16
0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)), 0o305 | 0o325 | 0o345 | 0o365 => PUSH(group3((byte >> 4) & 0x03)),
0o315 => CALL(JumpCondition::Always), // CALL u16
0o315 => CALL(JpCond::Always),
// ADD, ADC, SUB, SBC, AND, XOR, OR, and CP
0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => { 0o306 | 0o316 | 0o326 | 0o336 | 0o346 | 0o356 | 0o366 | 0o376 => {
alu_imm_instr((byte >> 3) & 0x07) alu_imm_instr((byte >> 3) & 0x07)
} }
0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000), 0o307 | 0o317 | 0o327 | 0o337 | 0o347 | 0o357 | 0o367 | 0o377 => RST(byte & 0b00111000),
_ => panic!("{:#04X} is an illegal opcode", byte), _ => Invalid, // 0xCB is 0o313
} }
} }
@@ -1681,7 +1684,7 @@ impl Instruction {
mod jump { mod jump {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) enum JumpCondition { pub(crate) enum JpCond {
Always, Always,
NotZero, NotZero,
Zero, Zero,
@@ -1689,9 +1692,9 @@ mod jump {
Carry, Carry,
} }
impl std::fmt::Debug for JumpCondition { impl std::fmt::Debug for JpCond {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use JumpCondition::*; use JpCond::*;
match self { match self {
Always => f.write_str(""), Always => f.write_str(""),
@@ -1704,14 +1707,14 @@ mod jump {
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub(crate) enum JumpLocation { pub(crate) enum JpLoc {
HL, HL,
ImmediateWord, ImmediateWord,
} }
impl std::fmt::Debug for JumpLocation { impl std::fmt::Debug for JpLoc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use JumpLocation::*; use JpLoc::*;
match *self { match *self {
HL => f.write_str("HL"), HL => f.write_str("HL"),
@@ -1871,7 +1874,7 @@ mod load {
mod table { mod table {
use super::add::{Source as AddSource, Target as AddTarget}; use super::add::{Source as AddSource, Target as AddTarget};
use super::alu::Source as AluSource; use super::alu::Source as AluSource;
use super::{Instruction, JumpCondition}; use super::{Instruction, JpCond};
use crate::cpu::{Register as CpuRegister, RegisterPair}; use crate::cpu::{Register as CpuRegister, RegisterPair};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -2070,12 +2073,12 @@ mod table {
0b101 => L, 0b101 => L,
0b110 => IndirectHL, 0b110 => IndirectHL,
0b111 => A, 0b111 => A,
_ => unreachable!("{:#04X} is not a valid Register"), _ => unreachable!("{:#04X} is not a valid Register", code),
} }
} }
pub(crate) fn jump_cond(code: u8) -> JumpCondition { pub(crate) fn jump_cond(code: u8) -> JpCond {
use JumpCondition::*; use JpCond::*;
match code { match code {
0b00 => NotZero, 0b00 => NotZero,
@@ -2152,20 +2155,23 @@ mod table {
} }
pub(crate) mod dbg { pub(crate) mod dbg {
use std::borrow::Cow;
use super::add::{Source as AddSource, Target as AddTarget}; use super::add::{Source as AddSource, Target as AddTarget};
use super::jump::JumpCondition; use super::alu::Source as AluSource;
use super::jump::{JpCond, JpLoc};
use super::load::{Source as LDSource, Target as LDTarget}; use super::load::{Source as LDSource, Target as LDTarget};
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 {
let opcode = cpu.read_byte(pc); let opcode = cpu.read_byte(pc);
pc += 1; pc += 1;
let instr = if opcode == 0xCB { let maybe_instr = if opcode == 0xCB {
let opcode = cpu.read_byte(pc); let opcode = cpu.read_byte(pc);
pc += 1; pc += 1;
@@ -2174,44 +2180,160 @@ pub(crate) mod dbg {
Instruction::unprefixed(opcode) Instruction::unprefixed(opcode)
}; };
let instr_asm = format!("{:04X} {:?}\n", pc - 1, instr); match maybe_instr {
asm.push_str(&instr_asm); Instruction::Invalid => {}
instr => {
pc += delta::pc_inc_count(instr) let asm = format!("{:04X} {:?}\n", pc - 1, instr);
sm83_asm.push_str(&asm);
pc += delta::pc_inc_count(instr)
}
}
} }
asm sm83_asm
} }
fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> String { pub(crate) fn new_disasm(cpu: &Cpu, limit: u8) -> String {
let mut assembly = String::new();
let mut pc = cpu.register_pair(RegisterPair::PC);
for _ in 0..limit {
let opcode = cpu.read_byte(pc);
pc += 1;
let maybe_instr = if opcode == 0xCB {
let opcode = cpu.read_byte(pc);
pc += 1;
Instruction::prefixed(opcode)
} else {
Instruction::unprefixed(opcode)
};
match maybe_instr {
Instruction::Invalid => {}
instr => {
let output = format!("${:04X} {}\n", pc - 1, disasm(cpu, pc, instr));
assembly.push_str(&output);
pc += delta::pc_inc_count(instr);
}
}
}
assembly
}
// TODO: It might be better if I pass in a mutable writer instead of usnig a String
fn disasm(cpu: &Cpu, pc: u16, instr: Instruction) -> Cow<str> {
use Instruction::*; use Instruction::*;
let imm_byte = cpu.read_byte(pc + 1); let imm_byte = cpu.read_byte(pc + 1);
let imm_word = (cpu.read_byte(pc + 2) as u16) << 8 | imm_byte as u16; let imm_word = (cpu.read_byte(pc + 2) as u16) << 8 | imm_byte as u16;
match instr { match instr {
NOP => format!("NOP"), // Unprefixed Instructions
NOP => "NOP".into(),
LD(LDTarget::IndirectImmediateWord, LDSource::SP) => { LD(LDTarget::IndirectImmediateWord, LDSource::SP) => {
format!("LD ({:#06X}), SP", imm_word) format!("LD ({:#06X}), SP", imm_word).into()
} }
STOP => format!("STOP"), STOP => "STOP".into(),
JR(JumpCondition::Always) => format!("JR {}", imm_byte as i8), JR(JpCond::Always) => format!("JR {}", imm_byte as i8).into(),
JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8), JR(cond) => format!("JR {:?} {}", cond, imm_byte as i8).into(),
LD(LDTarget::Group1(rp), LDSource::ImmediateWord) => { LD(LDTarget::Group1(rp), LDSource::ImmediateWord) => {
format!("LD {:?} {:#06X}", rp, imm_word) format!("LD {:?} {:#06X}", rp, imm_word).into()
} }
ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp), ADD(AddTarget::HL, AddSource::Group1(rp)) => format!("ADD HL, {:?}", rp).into(),
LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp), LD(LDTarget::IndirectGroup2(rp), LDSource::A) => format!("LD ({:?}), A", rp).into(),
LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp), LD(LDTarget::A, LDSource::IndirectGroup2(rp)) => format!("LD A, ({:?})", rp).into(),
INC(AllRegisters::Group1(rp)) => format!("INC {:?}", rp).into(),
DEC(AllRegisters::Group1(rp)) => format!("DEC {:?}", rp).into(),
INC(AllRegisters::Register(reg)) => format!("INC {:?}", reg).into(),
DEC(AllRegisters::Register(reg)) => format!("DEC {:?}", reg).into(),
LD(LDTarget::Register(reg), LDSource::ImmediateByte) => {
format!("LD {:?}, {:#04X}", reg, imm_byte).into()
}
RLCA => "RLCA".into(),
RRCA => "RRCA".into(),
RLA => "RLA".into(),
RRA => "RRA".into(),
DAA => "DAA".into(),
CPL => "CPL".into(),
SCF => "SCF".into(),
CCF => "CCF".into(),
HALT => "HALT".into(),
LD(LDTarget::Register(left), LDSource::Register(right)) => {
format!("LD {:?}, {:?}", left, right).into()
}
ADD(AddTarget::A, AddSource::Register(reg)) => format!("ADD A, {:?}", reg).into(),
ADC(AluSource::Register(reg)) => format!("ADC {:?}", reg).into(),
SUB(AluSource::Register(reg)) => format!("SUB {:?}", reg).into(),
SBC(AluSource::Register(reg)) => format!("SBC {:?}", reg).into(),
AND(AluSource::Register(reg)) => format!("AND {:?}", reg).into(),
XOR(AluSource::Register(reg)) => format!("XOR {:?}", reg).into(),
OR(AluSource::Register(reg)) => format!("OR {:?}", reg).into(),
CP(AluSource::Register(reg)) => format!("CP {:?}", reg).into(),
RET(JpCond::Always) => "RET".into(),
RET(cond) => format!("RET {:?}", cond).into(),
LD(LDTarget::IoWithImmediateOffset, LDSource::A) => {
format!("LD ({:#06X}) , A", 0xFF00 + imm_byte as u16).into()
}
ADD(AddTarget::SP, AddSource::ImmediateSignedByte) => {
format!("ADD SP, {}", imm_byte as i8).into()
}
LD(LDTarget::A, LDSource::IoWithImmediateOffset) => {
format!("LD A, ({:#06X})", 0xFF00 + imm_byte as u16).into()
}
LDHL => format!("LD HL, SP + {}", imm_byte as i8).into(),
POP(rp) => format!("POP {:?}", rp).into(),
RETI => "RETI".into(),
JP(JpCond::Always, JpLoc::HL) => "JP HL".into(),
LD(LDTarget::SP, LDSource::HL) => "LD SP, HL".into(),
JP(JpCond::Always, JpLoc::ImmediateWord) => format!("JP {:#06X}", imm_word).into(),
JP(cond, JpLoc::ImmediateWord) => format!("JP {:?} {:#06X}", cond, imm_word).into(),
LD(LDTarget::IoWithC, LDSource::A) => "LD (0xFF00 + C), A".into(),
LD(LDTarget::IndirectImmediateWord, LDSource::A) => {
format!("LD ({:#06X}), A", imm_byte).into()
}
LD(LDTarget::A, LDSource::IoWithC) => "LD A, (0xFF00 + C)".into(),
LD(LDTarget::A, LDSource::IndirectImmediateWord) => {
format!("LD A, ({:#06X})", imm_word).into()
}
DI => "DI".into(),
EI => "EI".into(),
CALL(cond) => format!("CALL {:?} {:#06X}", cond, imm_word).into(),
PUSH(rp) => format!("PUSH {:?}", rp).into(),
ADD(AddTarget::A, AddSource::ImmediateByte) => {
format!("ADD A, {:#04X}", imm_byte).into()
}
ADC(AluSource::ImmediateByte) => format!("ADC {:#04X}", imm_byte).into(),
SUB(AluSource::ImmediateByte) => format!("SUB {:#04X}", imm_byte).into(),
SBC(AluSource::ImmediateByte) => format!("SBC {:#04X}", imm_byte).into(),
AND(AluSource::ImmediateByte) => format!("AND {:#04X}", imm_byte).into(),
XOR(AluSource::ImmediateByte) => format!("XOR {:#04X}", imm_byte).into(),
OR(AluSource::ImmediateByte) => format!("OR {:#04X}", imm_byte).into(),
CP(AluSource::ImmediateByte) => format!("CP {:#04X}", imm_byte).into(),
RST(v) => format!("RST {:#04X}", v).into(),
_ => todo!(), // Prefixed Instructions
RLC(reg) => format!("RLC {:?}", reg).into(),
RRC(reg) => format!("RRC {:?}", reg).into(),
RL(reg) => format!("RL {:?}", reg).into(),
RR(reg) => format!("RR {:?}", reg).into(),
SLA(reg) => format!("SLA {:?}", reg).into(),
SRA(reg) => format!("SRA {:?}", reg).into(),
SWAP(reg) => format!("SWAP {:?}", reg).into(),
SRL(reg) => format!("SRL {:?}", reg).into(),
BIT(bit, reg) => format!("BIT {}, {:?}", bit, reg).into(),
RES(bit, reg) => format!("RES {}, {:?}", bit, reg).into(),
SET(bit, reg) => format!("SET {}, {:?}", bit, reg).into(),
_ => unreachable!("{:?} is an illegal instruction", instr),
} }
} }
mod delta { mod delta {
use super::super::add::{Source as AddSource, Target as AddTarget}; use super::super::add::{Source as AddSource, Target as AddTarget};
use super::super::alu::Source as AluSource; use super::super::alu::Source as AluSource;
use super::super::jump::{JumpCondition, JumpLocation}; use super::super::jump::{JpCond, JpLoc};
use super::super::load::{Source as LDSource, Target as LDTarget}; use super::super::load::{Source as LDSource, Target as LDTarget};
use super::super::{AllRegisters, Instruction}; use super::super::{AllRegisters, Instruction};
@@ -2258,9 +2380,9 @@ pub(crate) mod dbg {
LDHL => 1, LDHL => 1,
POP(_) => 0, POP(_) => 0,
RETI => 0, RETI => 0,
JP(JumpCondition::Always, JumpLocation::HL) => 0, JP(JpCond::Always, JpLoc::HL) => 0,
LD(LDTarget::SP, LDSource::HL) => 0, LD(LDTarget::SP, LDSource::HL) => 0,
JP(_, JumpLocation::ImmediateWord) => 2, JP(_, JpLoc::ImmediateWord) => 2,
LD(LDTarget::IoWithC, LDSource::A) => 0, LD(LDTarget::IoWithC, LDSource::A) => 0,
LD(LDTarget::IndirectImmediateWord, LDSource::A) => 2, LD(LDTarget::IndirectImmediateWord, LDSource::A) => 2,
LD(LDTarget::A, LDSource::IoWithC) => 0, LD(LDTarget::A, LDSource::IoWithC) => 0,

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

@@ -1,38 +1,28 @@
use std::path::PathBuf;
use std::time::Instant; use std::time::Instant;
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg}; use clap::{arg, command, value_parser};
use egui_wgpu_backend::RenderPass; use gb::gui::{EmuMode, Gui};
use gb::{emu, gui}; use gb::{emu, gui};
use gilrs::Gilrs; use gilrs::Gilrs;
use gui::GuiState;
use rodio::{OutputStream, Sink}; use rodio::{OutputStream, Sink};
use tracing_subscriber::EnvFilter; use tracing_subscriber::EnvFilter;
use winit::event::{Event, WindowEvent}; use winit::event::{Event, WindowEvent};
use winit::event_loop::EventLoop; use winit::event_loop::{EventLoop, EventLoopBuilder};
const AUDIO_ENABLED: bool = true; const AUDIO_ENABLED: bool = true;
fn main() { fn main() {
let app = App::new(crate_name!()) let m = command!()
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!());
let m = app
.arg( .arg(
Arg::with_name("rom") arg!(-b --boot <FILE> "path to boot ROM")
.value_name("ROM_FILE") .required(false)
.takes_value(true) .value_parser(value_parser!(PathBuf)),
.index(1)
.help("Path to the Game ROM"),
) )
.arg( .arg(
Arg::with_name("boot") arg!([ROM_FILE] "path to game ROM")
.short("b") .required(true)
.long("boot") .value_parser(value_parser!(PathBuf)),
.value_name("FILE")
.takes_value(true)
.help("Path to Boot ROM"),
) )
.get_matches(); .get_matches();
@@ -45,25 +35,8 @@ fn main() {
.with_env_filter(EnvFilter::from_default_env()) .with_env_filter(EnvFilter::from_default_env())
.init(); .init();
// --Here lies a lot of winit + wgpu Boilerplate-- // Init CPU
let event_loop: EventLoop<Event<()>> = EventLoop::with_user_event(); let mut cpu = match m.get_one::<PathBuf>("boot") {
let window = gui::build_window(&event_loop).expect("build window");
let (instance, surface) = gui::create_surface(&window);
let adapter = gui::request_adapter(&instance, &surface).expect("request adaptor");
let (device, queue) = gui::request_device(&adapter).expect("request device");
let format = surface
.get_preferred_format(&adapter)
.expect("get surface format");
let mut config = gui::surface_config(&window, format);
surface.configure(&device, &config);
let mut platform = gui::platform_desc(&window);
let mut render_pass = RenderPass::new(&device, format, 1);
// We interrupt your boiler plate to initialize the emulator so that
// we can copy it's empty pixel buffer to the GPU
let mut cpu = match m.value_of("boot") {
Some(path) => { Some(path) => {
tracing::info!("User-provided boot ROM"); tracing::info!("User-provided boot ROM");
emu::from_boot_rom(path).expect("initialize emulator with custom boot rom") emu::from_boot_rom(path).expect("initialize emulator with custom boot rom")
@@ -74,20 +47,12 @@ fn main() {
} }
}; };
// Set up the wgpu (and then EGUI) texture we'll be working with.
let texture_size = gui::texture_size();
let texture = gui::create_texture(&device, texture_size);
gui::write_to_texture(&queue, &texture, emu::pixel_buf(&cpu), texture_size);
let texture_id = gui::expose_texture_to_egui(&mut render_pass, &device, &texture);
// Load ROM if filepath was provided // Load ROM if filepath was provided
if let Some(path) = m.value_of("rom") { if let Some(path) = m.get_one::<PathBuf>("ROM_FILE") {
tracing::info!("User-provided cartridge ROM"); tracing::info!("User-provided cartridge ROM");
emu::read_game_rom(&mut cpu, path).expect("read game rom from path"); emu::read_game_rom(&mut cpu, path).expect("read game rom from path");
} }
emu::load_save(&mut cpu); emu::load_save(&mut cpu);
let rom_title = emu::rom_title(&cpu).to_string(); let rom_title = emu::rom_title(&cpu).to_string();
tracing::info!("Initialize Gamepad"); tracing::info!("Initialize Gamepad");
@@ -114,80 +79,44 @@ fn main() {
} }
// Set up state for the Immediate-mode GUI // Set up state for the Immediate-mode GUI
let mut app = GuiState::new(rom_title); let event_loop: EventLoop<Event<()>> = EventLoopBuilder::with_user_event().build();
let mut last_key = gui::unused_key();
let mut app = Gui::new(rom_title, &event_loop, &cpu);
let mut last_key = gui::unused_key(); // TODO: Fix this awful impl
// used for egui animations // used for egui animations
let start_time = Instant::now(); let start_time = Instant::now();
event_loop.run(move |event, _, control_flow| { event_loop.run(move |event, _, control_flow| {
platform.handle_event(&event); app.handle_event(&event);
match event { match event {
Event::MainEventsCleared => { Event::MainEventsCleared => {
if app.quit { app.maybe_quit(&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);
}
_ => {}
};
window.request_redraw(); // Input has been consumed, reset it
last_key = gui::unused_key();
app.request_redraw();
} }
Event::RedrawRequested(..) => { Event::RedrawRequested(..) => {
platform.update_time(start_time.elapsed().as_secs_f64()); app.update_time(start_time.elapsed().as_secs_f64());
app.paint(&cpu);
let data = emu::pixel_buf(&cpu);
gui::write_to_texture(&queue, &texture, data, texture_size);
let output_frame = match surface.get_current_texture() {
Ok(frame) => frame,
Err(e) => {
eprintln!("Dropped frame with error: {}", e);
return;
}
};
let output_view = gui::create_view(&output_frame);
// Begin to draw Egui components
platform.begin_frame();
gui::draw_egui(&cpu, &mut app, &platform.context(), texture_id);
// End the UI frame. We could now handle the output and draw the UI with the backend.
let (_, paint_commands) = platform.end_frame(Some(&window));
let paint_jobs = platform.context().tessellate(paint_commands);
let mut encoder = gui::create_command_encoder(&device);
let screen_descriptor = gui::create_screen_descriptor(&window, &config);
// Upload all resources for the GPU.
render_pass.update_texture(&device, &queue, &platform.context().texture());
render_pass.update_user_textures(&device, &queue);
render_pass.update_buffers(&device, &queue, &paint_jobs, &screen_descriptor);
// Record all render passes.
gui::execute_render_pass(
&mut render_pass,
&mut encoder,
&output_view,
paint_jobs,
&screen_descriptor,
)
.expect("record render passes");
// Submit the commands.
queue.submit(std::iter::once(encoder.finish()));
// Redraw egui
output_frame.present();
} }
Event::WindowEvent { event, .. } => match event { Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(size) => { WindowEvent::Resized(size) => app.resize(size),
config.width = size.width; WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow),
config.height = size.height;
surface.configure(&device, &config);
}
WindowEvent::CloseRequested => {
emu::save_and_exit(&cpu, control_flow);
}
WindowEvent::KeyboardInput { input, .. } => last_key = input, WindowEvent::KeyboardInput { input, .. } => last_key = input,
_ => {} _ => {}
}, },

View File

@@ -10,6 +10,8 @@ use types::{
ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress, ObjectPaletteKind, ObjectSize, Pixels, RenderPriority, TileDataAddress,
}; };
use once_cell::sync::Lazy;
mod dma; mod dma;
mod types; mod types;
@@ -32,6 +34,14 @@ const LIGHT_GRAY: [u8; 4] = 0xAEBA89FFu32.to_be_bytes();
const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes(); const DARK_GRAY: [u8; 4] = 0x5E6745FFu32.to_be_bytes();
const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes(); const BLACK: [u8; 4] = 0x202020FFu32.to_be_bytes();
static BLANK_SCREEN: Lazy<Box<[u8; (GB_WIDTH * 4) * GB_HEIGHT]>> = Lazy::new(|| {
WHITE
.repeat(GB_WIDTH * GB_HEIGHT)
.into_boxed_slice()
.try_into()
.unwrap()
});
#[derive(Debug)] #[derive(Debug)]
pub struct Ppu { pub struct Ppu {
pub(crate) int: Interrupt, pub(crate) int: Interrupt,
@@ -48,7 +58,7 @@ pub struct Ppu {
fetch: PixelFetcher, fetch: PixelFetcher,
fifo: PixelFifo, fifo: PixelFifo,
obj_buffer: ObjectBuffer, obj_buffer: ObjectBuffer,
pub(crate) frame_buf: Box<[u8; GB_WIDTH * GB_HEIGHT * 4]>, pub(crate) frame_buf: FrameBuffer,
win_stat: WindowStatus, win_stat: WindowStatus,
scanline_start: bool, scanline_start: bool,
@@ -70,15 +80,26 @@ impl BusIo for Ppu {
impl Ppu { impl Ppu {
pub(crate) fn tick(&mut self) { pub(crate) fn tick(&mut self) {
self.dot += 1;
if !self.ctrl.lcd_enabled() { if !self.ctrl.lcd_enabled() {
if self.dot > 0 {
// Check ensures this expensive operation only happens once
self.frame_buf
.get_mut(Device::Guest)
.copy_from_slice(BLANK_SCREEN.as_ref());
}
self.stat.set_mode(PpuMode::HBlank);
self.pos.line_y = 0;
self.dot = 0;
return; return;
} }
self.dot += 1;
match self.stat.mode() { match self.stat.mode() {
PpuMode::OamScan => { PpuMode::OamScan => {
// Cycles 1 -> 80 // Cycles 1 -> 80
if self.dot >= 80 { if self.dot >= 80 {
self.x_pos = 0; self.x_pos = 0;
self.scanline_start = true; self.scanline_start = true;
@@ -104,12 +125,7 @@ impl Ppu {
self.scan_oam(); self.scan_oam();
} }
PpuMode::Drawing => { PpuMode::Drawing => {
if self.ctrl.lcd_enabled() { self.draw();
// Only Draw when the LCD Is Enabled
self.draw();
} else {
self.reset();
}
if self.x_pos == 160 { if self.x_pos == 160 {
if self.stat.hblank_int() { if self.stat.hblank_int() {
@@ -164,6 +180,9 @@ impl Ppu {
self.int.set_lcd_stat(true); self.int.set_lcd_stat(true);
} }
// Screen is done drawing
self.frame_buf.swap();
PpuMode::VBlank PpuMode::VBlank
} else { } else {
if self.stat.oam_int() { if self.stat.oam_int() {
@@ -397,8 +416,8 @@ impl Ppu {
let x = self.x_pos as usize; let x = self.x_pos as usize;
let i = (GB_WIDTH * 4) * y + (x * 4); let i = (GB_WIDTH * 4) * y + (x * 4);
self.frame_buf[i..(i + rgba.len())].copy_from_slice(&rgba);
self.frame_buf.get_mut(Device::Guest)[i..(i + rgba.len())].copy_from_slice(&rgba);
self.x_pos += 1; self.x_pos += 1;
} }
@@ -415,14 +434,6 @@ impl Ppu {
} }
} }
fn reset(&mut self) {
self.pos.line_y = 0;
self.stat.set_mode(PpuMode::HBlank);
let mut blank = WHITE.repeat(self.frame_buf.len() / 4);
self.frame_buf.swap_with_slice(&mut blank);
}
fn clock_fifo(&mut self) -> Option<GrayShade> { fn clock_fifo(&mut self) -> Option<GrayShade> {
use RenderPriority::*; use RenderPriority::*;
@@ -468,7 +479,7 @@ impl Default for Ppu {
Self { Self {
vram: Box::new([0u8; VRAM_SIZE]), vram: Box::new([0u8; VRAM_SIZE]),
dot: Default::default(), dot: Default::default(),
frame_buf: Box::new([0; GB_WIDTH * GB_HEIGHT * 4]), frame_buf: FrameBuffer::new().expect("create frame buffers"),
int: Default::default(), int: Default::default(),
ctrl: LCDControl(0), ctrl: LCDControl(0),
monochrome: Default::default(), monochrome: Default::default(),
@@ -910,3 +921,96 @@ 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};
use crate::Cycle;
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
}
pub(crate) fn dot(ppu: &Ppu) -> Cycle {
ppu.dot
}
}
#[derive(Debug)]
pub struct FrameBuffer {
buf: [Box<[u8; Self::FRAME_LEN]>; 2],
current: bool,
}
#[derive(PartialEq)]
pub enum Device {
Guest,
Host,
}
impl FrameBuffer {
const FRAME_LEN: usize = GB_WIDTH * std::mem::size_of::<u32>() * GB_HEIGHT;
pub fn new() -> Result<Self, FrameBufferError> {
Ok(Self {
buf: [
vec![0; Self::FRAME_LEN]
.into_boxed_slice()
.try_into()
.map_err(|_| FrameBufferError::TryFrom)?,
vec![0; Self::FRAME_LEN]
.into_boxed_slice()
.try_into()
.map_err(|_| FrameBufferError::TryFrom)?,
],
current: false,
})
}
pub fn swap(&mut self) {
self.current = !self.current;
}
pub fn get_mut(&mut self, device: Device) -> &mut [u8; Self::FRAME_LEN] {
let idx = match device {
Device::Guest => self.current,
Device::Host => !self.current,
};
&mut *self.buf[idx as usize]
}
pub fn get(&self, device: Device) -> &[u8; Self::FRAME_LEN] {
let idx = match device {
Device::Guest => self.current,
Device::Host => !self.current,
};
&*self.buf[idx as usize]
}
}
#[derive(Debug, thiserror::Error)]
pub enum FrameBufferError {
#[error("Failed to coerce boxed slice to boxed array")]
TryFrom,
}

View File

@@ -335,7 +335,7 @@ impl From<ObjectPaletteKind> for u8 {
} }
} }
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum RenderPriority { pub enum RenderPriority {
Object = 0, Object = 0,
BackgroundAndWindow = 1, BackgroundAndWindow = 1,