Compare commits
9 Commits
84ddf323ec
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a352ca807 | |||
| 068e4124a6 | |||
| fedae65cc8 | |||
| 41586d8cc4 | |||
| 79740ac245 | |||
| d6bfde081e | |||
| d2f57ee66b | |||
| 3841d3a98c | |||
| 85940c8744 |
2164
Cargo.lock
generated
2164
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -8,17 +8,16 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bitfield = "0.14"
|
bitfield = "0.17"
|
||||||
clap = { version = "3.1", features = ["cargo"] }
|
clap = { version = "4.0", features = ["cargo"] }
|
||||||
gilrs = "0.9"
|
gilrs = "0.11"
|
||||||
winit = "0.27"
|
winit = "0.28"
|
||||||
egui = "0.19"
|
egui = "0.21"
|
||||||
wgpu = "0.14"
|
egui_wgpu_backend = "0.22"
|
||||||
egui_wgpu_backend = "0.20"
|
egui_winit_platform = "0.18"
|
||||||
egui_winit_platform = "0.16"
|
pollster = "0.3"
|
||||||
pollster = "0.2"
|
rodio = "0.19"
|
||||||
rodio = "0.16"
|
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"] }
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -1,10 +1,9 @@
|
|||||||
# Rekai's Gameboy Emulator
|
# Rekai's Gameboy Emulator
|
||||||
[](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
|
||||||
--- | ---
|
--- | ---
|
||||||
|
|||||||
@@ -44,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> {
|
||||||
|
|||||||
49
src/gui.rs
49
src/gui.rs
@@ -1,11 +1,12 @@
|
|||||||
use egui::{Context, TextureId};
|
use egui::{Context, TextureId};
|
||||||
use egui_wgpu_backend::{RenderPass, ScreenDescriptor};
|
use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor};
|
||||||
use egui_winit_platform::Platform;
|
use egui_winit_platform::Platform;
|
||||||
use wgpu::{
|
use wgpu::{
|
||||||
Adapter, Backends, Color, CommandEncoder, CompositeAlphaMode, Device, Extent3d, FilterMode,
|
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
|
||||||
Instance, Queue, RequestDeviceError, Surface, SurfaceConfiguration, Texture, TextureFormat,
|
Surface, SurfaceCapabilities, SurfaceConfiguration, Texture, TextureView,
|
||||||
TextureUsages, TextureView, TextureViewDescriptor,
|
TextureViewDescriptor,
|
||||||
};
|
};
|
||||||
|
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
use winit::error::OsError;
|
use winit::error::OsError;
|
||||||
use winit::event::{ElementState, Event, KeyboardInput};
|
use winit::event::{ElementState, Event, KeyboardInput};
|
||||||
@@ -42,20 +43,26 @@ pub struct Gui {
|
|||||||
|
|
||||||
impl Gui {
|
impl Gui {
|
||||||
pub fn new<T>(title: String, event_loop: &EventLoop<T>, cpu: &Cpu) -> 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 window = build_window(event_loop).expect("build window");
|
||||||
|
|
||||||
let instance = Instance::new(Backends::PRIMARY);
|
let instance = Instance::new(InstanceDescriptor::default());
|
||||||
let surface = unsafe { instance.create_surface(&window) };
|
let surface = unsafe {
|
||||||
|
instance
|
||||||
|
.create_surface(&window)
|
||||||
|
.expect("create wgpu instance surface")
|
||||||
|
};
|
||||||
|
|
||||||
let adapter = request_adapter(&instance, &surface).expect("request adaptor");
|
let adapter = request_adapter(&instance, &surface).expect("request adaptor");
|
||||||
let (device, queue) = request_device(&adapter).expect("request device");
|
let (device, queue) = request_device(&adapter).expect("request device");
|
||||||
let texture_format = surface.get_supported_formats(&adapter)[0]; // First is preferred
|
|
||||||
|
|
||||||
let alpha_mode = surface.get_supported_alpha_modes(&adapter)[0];
|
let capabilities = surface.get_capabilities(&adapter);
|
||||||
let surface_config = surface_config(&window, alpha_mode, texture_format);
|
|
||||||
|
let surface_config = surface_config(&window, capabilities);
|
||||||
surface.configure(&device, &surface_config);
|
surface.configure(&device, &surface_config);
|
||||||
let platform = platform(&window);
|
let platform = platform(&window);
|
||||||
let mut render_pass = RenderPass::new(&device, texture_format, 1);
|
let mut render_pass = RenderPass::new(&device, surface_config.format, 1);
|
||||||
|
|
||||||
let texture_size = texture_size();
|
let texture_size = texture_size();
|
||||||
let texture = create_texture(&device, texture_size);
|
let texture = create_texture(&device, texture_size);
|
||||||
@@ -112,7 +119,7 @@ impl Gui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn paint(&mut self, cpu: &Cpu) {
|
pub fn paint(&mut self, cpu: &Cpu) {
|
||||||
use wgpu::SurfaceError;
|
use wgpu::{Color, SurfaceError};
|
||||||
|
|
||||||
let data = emu::pixel_buf(cpu);
|
let data = emu::pixel_buf(cpu);
|
||||||
write_to_texture(&self.queue, &self.texture, data, self.texture_size);
|
write_to_texture(&self.queue, &self.texture, data, self.texture_size);
|
||||||
@@ -350,21 +357,18 @@ fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceErr
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn surface_config(
|
fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration {
|
||||||
window: &Window,
|
use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages};
|
||||||
alpha_mode: CompositeAlphaMode,
|
|
||||||
format: TextureFormat,
|
|
||||||
) -> SurfaceConfiguration {
|
|
||||||
use wgpu::PresentMode;
|
|
||||||
|
|
||||||
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::Immediate,
|
present_mode: PresentMode::Fifo,
|
||||||
alpha_mode,
|
alpha_mode: capabilities.alpha_modes[0],
|
||||||
|
view_formats: vec![TextureFormat::Rgba8UnormSrgb],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,16 +395,17 @@ fn texture_size() -> Extent3d {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
32
src/main.rs
32
src/main.rs
@@ -1,6 +1,7 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use clap::{crate_authors, crate_description, crate_name, crate_version, Arg, Command};
|
use clap::{arg, command, value_parser};
|
||||||
use gb::gui::{EmuMode, Gui};
|
use gb::gui::{EmuMode, Gui};
|
||||||
use gb::{emu, gui};
|
use gb::{emu, gui};
|
||||||
use gilrs::Gilrs;
|
use gilrs::Gilrs;
|
||||||
@@ -12,26 +13,16 @@ use winit::event_loop::{EventLoop, EventLoopBuilder};
|
|||||||
const AUDIO_ENABLED: bool = true;
|
const AUDIO_ENABLED: bool = true;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let app = Command::new(crate_name!())
|
let m = command!()
|
||||||
.version(crate_version!())
|
|
||||||
.author(crate_authors!())
|
|
||||||
.about(crate_description!());
|
|
||||||
|
|
||||||
let m = app
|
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("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::new("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,7 +36,7 @@ fn main() {
|
|||||||
.init();
|
.init();
|
||||||
|
|
||||||
// Init CPU
|
// Init CPU
|
||||||
let mut cpu = match m.value_of("boot") {
|
let mut cpu = match m.get_one::<PathBuf>("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")
|
||||||
@@ -57,7 +48,7 @@ fn main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 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");
|
||||||
}
|
}
|
||||||
@@ -121,6 +112,7 @@ fn main() {
|
|||||||
}
|
}
|
||||||
Event::RedrawRequested(..) => {
|
Event::RedrawRequested(..) => {
|
||||||
app.update_time(start_time.elapsed().as_secs_f64());
|
app.update_time(start_time.elapsed().as_secs_f64());
|
||||||
|
app.paint(&cpu);
|
||||||
}
|
}
|
||||||
Event::WindowEvent { event, .. } => match event {
|
Event::WindowEvent { event, .. } => match event {
|
||||||
WindowEvent::Resized(size) => app.resize(size),
|
WindowEvent::Resized(size) => app.resize(size),
|
||||||
|
|||||||
73
src/ppu.rs
73
src/ppu.rs
@@ -58,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 * 4) * GB_HEIGHT]>,
|
pub(crate) frame_buf: FrameBuffer,
|
||||||
win_stat: WindowStatus,
|
win_stat: WindowStatus,
|
||||||
|
|
||||||
scanline_start: bool,
|
scanline_start: bool,
|
||||||
@@ -83,7 +83,9 @@ impl Ppu {
|
|||||||
if !self.ctrl.lcd_enabled() {
|
if !self.ctrl.lcd_enabled() {
|
||||||
if self.dot > 0 {
|
if self.dot > 0 {
|
||||||
// Check ensures this expensive operation only happens once
|
// Check ensures this expensive operation only happens once
|
||||||
self.frame_buf.copy_from_slice(BLANK_SCREEN.as_ref());
|
self.frame_buf
|
||||||
|
.get_mut(Device::Guest)
|
||||||
|
.copy_from_slice(BLANK_SCREEN.as_ref());
|
||||||
}
|
}
|
||||||
|
|
||||||
self.stat.set_mode(PpuMode::HBlank);
|
self.stat.set_mode(PpuMode::HBlank);
|
||||||
@@ -178,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() {
|
||||||
@@ -411,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,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(),
|
||||||
@@ -949,3 +954,63 @@ pub(crate) mod dbg {
|
|||||||
ppu.dot
|
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,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user