Compare commits
15 Commits
fc6516cbb2
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a352ca807 | |||
| 068e4124a6 | |||
| fedae65cc8 | |||
| 41586d8cc4 | |||
| 79740ac245 | |||
| d6bfde081e | |||
| d2f57ee66b | |||
| 3841d3a98c | |||
| 85940c8744 | |||
| c7adf54976 | |||
| f0f9003247 | |||
| 5eba097ad4 | |||
| 756c806c2a | |||
| 77af5453f0 | |||
| 41bc4f7afe |
2381
Cargo.lock
generated
2381
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -8,19 +8,21 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
bitfield = "0.13"
|
bitfield = "0.17"
|
||||||
clap = { version = "3.1", features = ["cargo"] }
|
clap = { version = "4.0", features = ["cargo"] }
|
||||||
gilrs = "0.9"
|
gilrs = "0.11"
|
||||||
egui-wgpu = "0.18"
|
winit = "0.28"
|
||||||
egui-winit = "0.18"
|
egui = "0.21"
|
||||||
pollster = "0.2"
|
egui_wgpu_backend = "0.22"
|
||||||
rodio = "0.15"
|
egui_winit_platform = "0.18"
|
||||||
rtrb = "0.2"
|
pollster = "0.3"
|
||||||
|
rodio = "0.19"
|
||||||
|
rtrb = "0.3"
|
||||||
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.12"
|
once_cell = "1.10"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
debug = true
|
debug = true
|
||||||
|
|||||||
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
|
||||||
--- | ---
|
--- | ---
|
||||||
|
|||||||
10
src/emu.rs
10
src/emu.rs
@@ -4,14 +4,14 @@ use crate::cartridge::Cartridge;
|
|||||||
use crate::cpu::Cpu;
|
use crate::cpu::Cpu;
|
||||||
use crate::{Cycle, GB_HEIGHT, GB_WIDTH};
|
use crate::{Cycle, GB_HEIGHT, GB_WIDTH};
|
||||||
use clap::crate_name;
|
use clap::crate_name;
|
||||||
use egui_winit::winit::event::KeyboardInput;
|
|
||||||
use egui_winit::winit::event_loop::ControlFlow;
|
|
||||||
use gilrs::Gilrs;
|
use gilrs::Gilrs;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use winit::event::KeyboardInput;
|
||||||
|
use winit::event_loop::ControlFlow;
|
||||||
|
|
||||||
pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED);
|
pub const SM83_CYCLE_TIME: Duration = Duration::from_nanos(1_000_000_000 / SM83_CLOCK_SPEED);
|
||||||
pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines
|
pub const CYCLES_IN_FRAME: Cycle = 456 * 154; // 456 Cycles times 154 scanlines
|
||||||
@@ -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> {
|
||||||
@@ -64,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
517
src/gui.rs
517
src/gui.rs
@@ -1,21 +1,20 @@
|
|||||||
use egui_wgpu::wgpu;
|
use egui::{Context, TextureId};
|
||||||
use egui_winit::egui;
|
use egui_wgpu_backend::{wgpu, RenderPass, ScreenDescriptor};
|
||||||
use egui_winit::winit;
|
use egui_winit_platform::Platform;
|
||||||
|
|
||||||
use egui::{ClippedPrimitive, Context, TextureId};
|
|
||||||
use egui_wgpu::renderer::{RenderPass, ScreenDescriptor};
|
|
||||||
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;
|
||||||
@@ -23,185 +22,163 @@ 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,
|
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,
|
mode: EmuMode::Running,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
window,
|
||||||
pub enum EmuMode {
|
platform,
|
||||||
Running,
|
|
||||||
StepFrame,
|
|
||||||
Stopped,
|
|
||||||
Step,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// To avoid using an [Option<KeyboardInput>] to keep track of user input from winit,
|
surface,
|
||||||
/// we can use a "default" value. However, in order for this to work the chosen "default"
|
device,
|
||||||
/// value must be an **unused** key, so that it is ignored by the emulator.
|
queue,
|
||||||
pub fn unused_key() -> KeyboardInput {
|
surface_config,
|
||||||
#![allow(deprecated)]
|
render_pass,
|
||||||
|
|
||||||
KeyboardInput {
|
|
||||||
scancode: Default::default(),
|
|
||||||
state: ElementState::Released,
|
|
||||||
virtual_keycode: Default::default(),
|
|
||||||
modifiers: Default::default(), // this argument is deprecated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
|
|
||||||
use winit::dpi::PhysicalSize;
|
|
||||||
use winit::window::WindowBuilder;
|
|
||||||
|
|
||||||
WindowBuilder::new()
|
|
||||||
.with_decorations(true)
|
|
||||||
.with_resizable(true)
|
|
||||||
.with_transparent(false)
|
|
||||||
.with_title(WINDOW_TITLE)
|
|
||||||
.with_inner_size(PhysicalSize {
|
|
||||||
width: EGUI_DIMENSIONS.0 as f32,
|
|
||||||
height: EGUI_DIMENSIONS.1 as f32,
|
|
||||||
})
|
|
||||||
.build(event_loop)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_surface(window: &Window) -> (Instance, Surface) {
|
|
||||||
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};
|
|
||||||
|
|
||||||
pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
|
|
||||||
power_preference: PowerPreference::HighPerformance,
|
|
||||||
force_fallback_adapter: false, // TODO: What do I want to do with this?
|
|
||||||
compatible_surface: Some(surface),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
|
|
||||||
use wgpu::{DeviceDescriptor, Features, Limits};
|
|
||||||
|
|
||||||
pollster::block_on(adapter.request_device(
|
|
||||||
&DeviceDescriptor {
|
|
||||||
label: None,
|
|
||||||
features: Features::default(),
|
|
||||||
limits: Limits::default(),
|
|
||||||
},
|
|
||||||
None,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn surface_config(window: &Window, format: TextureFormat) -> SurfaceConfiguration {
|
|
||||||
use wgpu::PresentMode;
|
|
||||||
|
|
||||||
let size = window.inner_size();
|
|
||||||
SurfaceConfiguration {
|
|
||||||
usage: TextureUsages::RENDER_ATTACHMENT,
|
|
||||||
format,
|
|
||||||
width: size.width as u32,
|
|
||||||
height: size.height as u32,
|
|
||||||
present_mode: PresentMode::Immediate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn texture_size() -> Extent3d {
|
|
||||||
Extent3d {
|
|
||||||
width: GB_WIDTH as u32,
|
|
||||||
height: GB_HEIGHT as u32,
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_texture(device: &Device, size: Extent3d) -> Texture {
|
|
||||||
use wgpu::{TextureDescriptor, TextureDimension};
|
|
||||||
|
|
||||||
device.create_texture(&TextureDescriptor {
|
|
||||||
size,
|
|
||||||
mip_level_count: 1,
|
|
||||||
sample_count: 1,
|
|
||||||
dimension: TextureDimension::D2,
|
|
||||||
format: TextureFormat::Rgba8UnormSrgb,
|
|
||||||
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
|
|
||||||
label: Some("gb_pixel_buffer"),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn write_to_texture(
|
|
||||||
queue: &Queue,
|
|
||||||
texture: &Texture,
|
|
||||||
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
|
|
||||||
size: Extent3d,
|
|
||||||
) {
|
|
||||||
use std::num::NonZeroU32;
|
|
||||||
use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureAspect};
|
|
||||||
|
|
||||||
queue.write_texture(
|
|
||||||
ImageCopyTexture {
|
|
||||||
texture,
|
texture,
|
||||||
mip_level: 0,
|
texture_size,
|
||||||
origin: Origin3d::ZERO,
|
texture_id,
|
||||||
aspect: TextureAspect::All,
|
}
|
||||||
},
|
}
|
||||||
data,
|
|
||||||
ImageDataLayout {
|
|
||||||
offset: 0,
|
|
||||||
bytes_per_row: NonZeroU32::new(4 * GB_WIDTH as u32),
|
|
||||||
rows_per_image: NonZeroU32::new(GB_HEIGHT as u32),
|
|
||||||
},
|
|
||||||
size,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
pub fn maybe_quit(&self, cpu: &Cpu, control_flow: &mut ControlFlow) {
|
||||||
pub fn create_view(frame: &SurfaceTexture) -> TextureView {
|
if self.should_quit {
|
||||||
use wgpu::TextureViewDescriptor;
|
emu::save_and_exit(cpu, control_flow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frame.texture.create_view(&TextureViewDescriptor::default())
|
pub fn request_redraw(&self) {
|
||||||
}
|
self.window.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
pub fn handle_event<T>(&mut self, event: &Event<T>) {
|
||||||
pub fn create_command_encoder(device: &Device) -> CommandEncoder {
|
self.platform.handle_event(event);
|
||||||
use wgpu::CommandEncoderDescriptor;
|
}
|
||||||
|
|
||||||
device.create_command_encoder(&CommandEncoderDescriptor {
|
pub fn update_time(&mut self, elapsed_seconds: f64) {
|
||||||
label: Some("encoder"),
|
self.platform.update_time(elapsed_seconds);
|
||||||
})
|
}
|
||||||
}
|
|
||||||
|
|
||||||
car#[inline]
|
pub fn resize(&mut self, size: PhysicalSize<u32>) {
|
||||||
pub fn execute_render_pass(
|
// See: https://github.com/rust-windowing/winit/issues/208
|
||||||
render_pass: &mut RenderPass,
|
if size.width > 0 && size.height > 0 {
|
||||||
encoder: &mut CommandEncoder,
|
self.surface_config.width = size.width;
|
||||||
view: &TextureView,
|
self.surface_config.height = size.height;
|
||||||
jobs: Vec<ClippedPrimitive>,
|
self.surface.configure(&self.device, &self.surface_config);
|
||||||
descriptor: &ScreenDescriptor,
|
}
|
||||||
) {
|
}
|
||||||
render_pass.execute(encoder, view, &jobs, descriptor, Some(wgpu::Color::BLACK))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
pub fn paint(&mut self, cpu: &Cpu) {
|
||||||
pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &Context, texture_id: TextureId) {
|
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};
|
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 {
|
||||||
@@ -211,11 +188,11 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &Context, texture_id: Textu
|
|||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
if ui.button("Quit").clicked() {
|
if ui.button("Quit").clicked() {
|
||||||
app.quit = true;
|
self.should_quit = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
egui::Window::new(&app.title).show(ctx, |ui| {
|
egui::Window::new(&self.title).show(ctx, |ui| {
|
||||||
ui.image(
|
ui.image(
|
||||||
texture_id,
|
texture_id,
|
||||||
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
|
[GB_WIDTH as f32 * SCALE, GB_HEIGHT as f32 * SCALE],
|
||||||
@@ -228,12 +205,12 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &Context, texture_id: Textu
|
|||||||
|
|
||||||
egui::Window::new("Settings").show(ctx, |ui| {
|
egui::Window::new("Settings").show(ctx, |ui| {
|
||||||
egui::ComboBox::from_label("Emulation Mode")
|
egui::ComboBox::from_label("Emulation Mode")
|
||||||
.selected_text(format!("{:?}", app.mode))
|
.selected_text(format!("{:?}", self.mode))
|
||||||
.show_ui(ui, |ui| {
|
.show_ui(ui, |ui| {
|
||||||
ui.selectable_value(&mut app.mode, EmuMode::Running, "Running");
|
ui.selectable_value(&mut self.mode, EmuMode::Running, "Running");
|
||||||
ui.selectable_value(&mut app.mode, EmuMode::Stopped, "Stopped");
|
ui.selectable_value(&mut self.mode, EmuMode::Stopped, "Stopped");
|
||||||
ui.selectable_value(&mut app.mode, EmuMode::StepFrame, "Step Frame");
|
ui.selectable_value(&mut self.mode, EmuMode::StepFrame, "Step Frame");
|
||||||
ui.selectable_value(&mut app.mode, EmuMode::Step, "Step");
|
ui.selectable_value(&mut self.mode, EmuMode::Step, "Step");
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -291,15 +268,13 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &Context, texture_id: Textu
|
|||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
let ie = cpu.int_enable();
|
let ie = cpu.int_enable();
|
||||||
|
|
||||||
let style = ctx.style();
|
// TODO: Reimplement this
|
||||||
let font_id = egui::TextStyle::Body.resolve(&style);
|
// let r_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'R');
|
||||||
|
// let e_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'E');
|
||||||
let r_len = ctx.fonts().glyph_width(&font_id, 'R');
|
// let q_len = ctx.fonts().glyph_width(egui::TextStyle::Body, 'Q');
|
||||||
let e_len = ctx.fonts().glyph_width(&font_id, 'E');
|
|
||||||
let q_len = ctx.fonts().glyph_width(&font_id, 'Q');
|
|
||||||
|
|
||||||
ui.label("IE:");
|
ui.label("IE:");
|
||||||
ui.add_space(q_len - (e_len - r_len));
|
// ui.add_space(q_len - (e_len - r_len));
|
||||||
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
|
let _ = ui.selectable_label(ie & 0b01 == 0x01, "VBlank");
|
||||||
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
|
let _ = ui.selectable_label(ie >> 1 & 0x01 == 0x01, "LCD STAT");
|
||||||
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
|
let _ = ui.selectable_label(ie >> 2 & 0x01 == 0x01, "Timer");
|
||||||
@@ -319,10 +294,176 @@ pub fn draw_egui(cpu: &Cpu, app: &mut GuiState, ctx: &Context, texture_id: Textu
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
/// 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.
|
||||||
|
pub fn unused_key() -> KeyboardInput {
|
||||||
|
#![allow(deprecated)]
|
||||||
|
|
||||||
|
KeyboardInput {
|
||||||
|
scancode: Default::default(),
|
||||||
|
state: ElementState::Released,
|
||||||
|
virtual_keycode: Default::default(),
|
||||||
|
modifiers: Default::default(), // this argument is deprecated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_window<T>(event_loop: &EventLoop<T>) -> Result<Window, OsError> {
|
||||||
|
use winit::window::WindowBuilder;
|
||||||
|
|
||||||
|
WindowBuilder::new()
|
||||||
|
.with_decorations(true)
|
||||||
|
.with_resizable(true)
|
||||||
|
.with_transparent(false)
|
||||||
|
.with_title(WINDOW_TITLE)
|
||||||
|
.with_inner_size(PhysicalSize {
|
||||||
|
width: EGUI_DIMENSIONS.0 as f32,
|
||||||
|
height: EGUI_DIMENSIONS.1 as f32,
|
||||||
|
})
|
||||||
|
.build(event_loop)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_adapter(instance: &Instance, surface: &Surface) -> Option<Adapter> {
|
||||||
|
use wgpu::{PowerPreference, RequestAdapterOptions};
|
||||||
|
|
||||||
|
pollster::block_on(instance.request_adapter(&RequestAdapterOptions {
|
||||||
|
power_preference: PowerPreference::HighPerformance,
|
||||||
|
compatible_surface: Some(surface),
|
||||||
|
force_fallback_adapter: false, // TODO: What do I want to do with this?
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_device(adapter: &Adapter) -> Result<(Device, Queue), RequestDeviceError> {
|
||||||
|
use wgpu::{DeviceDescriptor, Features, Limits};
|
||||||
|
|
||||||
|
pollster::block_on(adapter.request_device(
|
||||||
|
&DeviceDescriptor {
|
||||||
|
features: Features::default(),
|
||||||
|
limits: Limits::default(),
|
||||||
|
label: None,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn surface_config(window: &Window, capabilities: SurfaceCapabilities) -> SurfaceConfiguration {
|
||||||
|
use egui_wgpu_backend::wgpu::{PresentMode, TextureFormat, TextureUsages};
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
SurfaceConfiguration {
|
||||||
|
usage: TextureUsages::RENDER_ATTACHMENT,
|
||||||
|
format: capabilities.formats[0],
|
||||||
|
width: size.width as u32,
|
||||||
|
height: size.height as u32,
|
||||||
|
present_mode: PresentMode::Fifo,
|
||||||
|
alpha_mode: capabilities.alpha_modes[0],
|
||||||
|
view_formats: vec![TextureFormat::Rgba8UnormSrgb],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn platform(window: &Window) -> Platform {
|
||||||
|
use egui::FontDefinitions;
|
||||||
|
use egui_winit_platform::PlatformDescriptor;
|
||||||
|
|
||||||
|
let size = window.inner_size();
|
||||||
|
Platform::new(PlatformDescriptor {
|
||||||
|
physical_width: size.width as u32,
|
||||||
|
physical_height: size.height as u32,
|
||||||
|
scale_factor: window.scale_factor(),
|
||||||
|
font_definitions: FontDefinitions::default(),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn texture_size() -> Extent3d {
|
||||||
|
Extent3d {
|
||||||
|
width: GB_WIDTH as u32,
|
||||||
|
height: GB_HEIGHT as u32,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_texture(device: &Device, size: Extent3d) -> Texture {
|
||||||
|
use wgpu::{TextureDescriptor, TextureDimension, TextureFormat, TextureUsages};
|
||||||
|
|
||||||
|
device.create_texture(&TextureDescriptor {
|
||||||
|
size,
|
||||||
|
mip_level_count: 1,
|
||||||
|
sample_count: 1,
|
||||||
|
dimension: TextureDimension::D2,
|
||||||
|
format: TextureFormat::Bgra8Unorm,
|
||||||
|
usage: TextureUsages::COPY_DST | TextureUsages::TEXTURE_BINDING,
|
||||||
|
label: Some("gb_pixel_buffer"),
|
||||||
|
view_formats: &[TextureFormat::Bgra8Unorm],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn write_to_texture(
|
||||||
|
queue: &Queue,
|
||||||
|
texture: &Texture,
|
||||||
|
data: &[u8; GB_WIDTH * 4 * GB_HEIGHT],
|
||||||
|
size: Extent3d,
|
||||||
|
) {
|
||||||
|
use std::num::NonZeroU32;
|
||||||
|
use wgpu::{ImageCopyTexture, ImageDataLayout, Origin3d, TextureAspect};
|
||||||
|
|
||||||
|
queue.write_texture(
|
||||||
|
ImageCopyTexture {
|
||||||
|
texture,
|
||||||
|
mip_level: 0,
|
||||||
|
origin: Origin3d::ZERO,
|
||||||
|
aspect: TextureAspect::All,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
ImageDataLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: NonZeroU32::new(4 * GB_WIDTH as u32),
|
||||||
|
rows_per_image: NonZeroU32::new(GB_HEIGHT as u32),
|
||||||
|
},
|
||||||
|
size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expose_texture_to_egui(
|
||||||
|
render_pass: &mut RenderPass,
|
||||||
|
device: &Device,
|
||||||
|
view: &TextureView,
|
||||||
|
) -> TextureId {
|
||||||
|
render_pass.egui_texture_from_wgpu_texture(device, view, FILTER_MODE)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn create_command_encoder(device: &Device) -> CommandEncoder {
|
||||||
|
use wgpu::CommandEncoderDescriptor;
|
||||||
|
|
||||||
|
device.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("encoder"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn create_screen_descriptor(window: &Window, config: &SurfaceConfiguration) -> ScreenDescriptor {
|
||||||
|
ScreenDescriptor {
|
||||||
|
physical_width: config.width,
|
||||||
|
physical_height: config.height,
|
||||||
|
scale_factor: window.scale_factor() as f32,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod kbd {
|
pub mod kbd {
|
||||||
use egui_winit::winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
|
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
|
||||||
|
|
||||||
pub fn space_released(input: &KeyboardInput) -> bool {
|
pub fn space_released(input: &KeyboardInput) -> bool {
|
||||||
let keycode = input.virtual_keycode;
|
let keycode = input.virtual_keycode;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use egui_winit::winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
|
|
||||||
use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType};
|
use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType};
|
||||||
|
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Joypad {
|
pub struct Joypad {
|
||||||
|
|||||||
147
src/main.rs
147
src/main.rs
@@ -1,40 +1,28 @@
|
|||||||
|
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 egui_wgpu::renderer::RenderPass;
|
use gb::gui::{EmuMode, Gui};
|
||||||
use gb::gui::EmuMode;
|
|
||||||
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 egui_winit::winit::event::{Event, WindowEvent};
|
use winit::event_loop::{EventLoop, EventLoopBuilder};
|
||||||
use egui_winit::winit::event_loop::{EventLoop, EventLoopProxy};
|
|
||||||
|
|
||||||
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();
|
||||||
|
|
||||||
@@ -47,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("create winit 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 state = egui_winit::State::new(8192, &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")
|
||||||
@@ -76,22 +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 = todo!("Expose Texture ID to egui");
|
|
||||||
|
|
||||||
render_pass.
|
|
||||||
|
|
||||||
// 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");
|
||||||
@@ -118,19 +79,20 @@ 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 ctx = egui_winit::egui::Context::default();
|
|
||||||
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| {
|
||||||
|
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
match app.mode {
|
match app.mode {
|
||||||
EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key),
|
EmuMode::Running => emu::run_frame(&mut cpu, &mut gamepad, last_key),
|
||||||
@@ -146,69 +108,18 @@ fn main() {
|
|||||||
// Input has been consumed, reset it
|
// Input has been consumed, reset it
|
||||||
last_key = gui::unused_key();
|
last_key = gui::unused_key();
|
||||||
|
|
||||||
window.request_redraw();
|
app.request_redraw();
|
||||||
}
|
}
|
||||||
Event::RedrawRequested(..) => {
|
Event::RedrawRequested(..) => {
|
||||||
let data = emu::pixel_buf(&cpu);
|
app.update_time(start_time.elapsed().as_secs_f64());
|
||||||
gui::write_to_texture(&queue, &texture, data, texture_size);
|
app.paint(&cpu);
|
||||||
|
|
||||||
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, todo!(), todo!());
|
|
||||||
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, .. } => {
|
|
||||||
let exclusive_use = state.on_event(&ctx, &event);
|
|
||||||
if !exclusive_use {
|
|
||||||
match event {
|
|
||||||
WindowEvent::Resized(size) => {
|
|
||||||
config.width = size.width;
|
|
||||||
config.height = size.height;
|
|
||||||
surface.configure(&device, &config);
|
|
||||||
}
|
|
||||||
WindowEvent::CloseRequested => {
|
|
||||||
emu::save_and_exit(&cpu, control_flow);
|
|
||||||
}
|
}
|
||||||
|
Event::WindowEvent { event, .. } => match event {
|
||||||
|
WindowEvent::Resized(size) => app.resize(size),
|
||||||
|
WindowEvent::CloseRequested => emu::save_and_exit(&cpu, control_flow),
|
||||||
WindowEvent::KeyboardInput { input, .. } => last_key = input,
|
WindowEvent::KeyboardInput { input, .. } => last_key = input,
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user