chore: drop pixels-rs and add egui w/ wgpu backend

This commit is contained in:
2021-11-15 21:34:24 -04:00
parent ce5d58e1d2
commit 6f5e863645
9 changed files with 501 additions and 195 deletions

View File

@@ -73,7 +73,10 @@ impl Bus {
fn tick(&mut self, limit: u8) {
for _ in 0..limit {
self.timer.tick();
self.cart.as_mut().map(|cart| cart.tick());
if let Some(c) = self.cart.as_mut() {
c.tick()
}
self.ppu.tick();
self.apu.tick(self.timer.divider);
self.dma_tick()

View File

@@ -545,7 +545,7 @@ impl MBCIo for MBC3 {
}
Minute => self.rtc.min = byte & 0x3F,
Hour => self.rtc.hr = byte & 0x1F,
DayLow => self.rtc.day_low = byte & 0xFF,
DayLow => self.rtc.day_low = byte,
DayHigh => self.rtc.day_high = (byte & 0xC1).into(),
},
_ => {}

View File

@@ -8,14 +8,14 @@ use std::fs::File;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
use winit_input_helper::WinitInputHelper;
use winit::event::KeyboardInput;
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(crate) const SM83_CLOCK_SPEED: u64 = 0x40_0000; // Hz which is 4.194304Mhz
const DEFAULT_TITLE: &str = "DMG-01 Emulator";
pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper) -> Cycle {
pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: KeyboardInput) -> Cycle {
let mut elapsed = 0;
if let Some(event) = gamepad.next_event() {
@@ -30,8 +30,8 @@ pub fn run_frame(emu: &mut Emulator, gamepad: &mut Gilrs, key: &WinitInputHelper
elapsed
}
pub fn draw_frame(emu: &Emulator, buf: &mut [u8; GB_HEIGHT * GB_WIDTH * 4]) {
buf.copy_from_slice(emu.cpu.bus.ppu.frame_buf.as_ref());
pub fn pixel_buf(emu: &Emulator) -> &[u8; GB_HEIGHT * GB_WIDTH * 4] {
emu.cpu.bus.ppu.frame_buf.as_ref()
}
pub struct Emulator {

238
src/gui.rs Normal file
View File

@@ -0,0 +1,238 @@
use egui::{ClippedMesh, CtxRef, TextureId};
use egui_wgpu_backend::{BackendError, RenderPass, ScreenDescriptor};
use egui_winit_platform::Platform;
use wgpu::{
Adapter, CommandEncoder, Device, Extent3d, FilterMode, Instance, Queue, RequestDeviceError,
Surface, SurfaceConfiguration, SurfaceTexture, Texture, TextureFormat, TextureUsages,
TextureView,
};
use winit::error::OsError;
use winit::event::{ElementState, KeyboardInput};
use winit::event_loop::EventLoop;
use winit::window::Window;
use crate::{GB_HEIGHT, GB_WIDTH};
const EGUI_DIMENSIONS: (usize, usize) = (1280, 720);
const FILTER_MODE: FilterMode = FilterMode::Nearest;
const SCALE: f32 = 3.0;
/// Holds GUI State
#[derive(Debug, Clone)]
pub struct GuiState {
/// When true, egui winit should exit the application
pub quit: bool,
pub title: String,
}
impl GuiState {
pub fn new(title: String) -> Self {
Self {
title,
quit: Default::default(),
}
}
}
/// This is meant to serve as a Default value. Therefore this KeyboardInput should **always** represent an **unused** key
/// performing an **unused** action
///
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
}
}
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("DMG-01 Emulator")
.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::Mailbox,
}
}
pub fn platform_desc(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()
})
}
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,
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,
);
}
pub fn expose_texture_to_egui(
render_pass: &mut RenderPass,
device: &Device,
texture: &Texture,
) -> TextureId {
render_pass.egui_texture_from_wgpu_texture(device, texture, FILTER_MODE)
}
#[inline]
pub fn create_view(frame: &SurfaceTexture) -> TextureView {
use wgpu::TextureViewDescriptor;
frame.texture.create_view(&TextureViewDescriptor::default())
}
#[inline]
pub fn create_command_encoder(device: &Device) -> CommandEncoder {
use wgpu::CommandEncoderDescriptor;
device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("encoder"),
})
}
#[inline]
pub 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,
}
}
#[inline]
pub fn execute_render_pass(
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 draw_egui(app: &mut GuiState, ctx: &CtxRef, texture_id: TextureId) {
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],
);
})
});
}

View File

@@ -1,5 +1,5 @@
use gilrs::{Button, Event as GamepadEvent, EventType as GamepadEventType};
use winit_input_helper::WinitInputHelper;
use winit::event::{ElementState, KeyboardInput, VirtualKeyCode};
#[derive(Debug)]
pub struct Joypad {
@@ -111,68 +111,33 @@ impl ButtonEvent {
}
#[inline]
pub(crate) fn handle_keyboard_input(pad: &mut Joypad, input: &WinitInputHelper) {
use winit::event::VirtualKeyCode;
// TODO: What do I have to do to get a match statement here?
pub(crate) fn handle_keyboard_input(pad: &mut Joypad, key: KeyboardInput) {
let state = &mut pad.ext;
let irq = &mut pad.interrupt;
if input.key_pressed(VirtualKeyCode::Down) {
state.dpad_down.update(true, irq);
}
if input.key_released(VirtualKeyCode::Down) {
state.dpad_down.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Up) {
state.dpad_up.update(true, irq);
}
if input.key_released(VirtualKeyCode::Up) {
state.dpad_up.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Left) {
state.dpad_left.update(true, irq);
}
if input.key_released(VirtualKeyCode::Left) {
state.dpad_left.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Right) {
state.dpad_right.update(true, irq);
}
if input.key_released(VirtualKeyCode::Right) {
state.dpad_right.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Return) {
state.start.update(true, irq);
}
if input.key_released(VirtualKeyCode::Return) {
state.start.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::RShift) {
state.select.update(true, irq);
}
if input.key_released(VirtualKeyCode::RShift) {
state.select.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::Z) {
state.south.update(true, irq);
}
if input.key_released(VirtualKeyCode::Z) {
state.south.update(false, irq);
}
if input.key_pressed(VirtualKeyCode::X) {
state.east.update(true, irq);
}
if input.key_released(VirtualKeyCode::X) {
state.east.update(false, irq);
match key.state {
ElementState::Pressed => match key.virtual_keycode {
Some(VirtualKeyCode::Down) => state.dpad_down.update(true, irq),
Some(VirtualKeyCode::Up) => state.dpad_up.update(true, irq),
Some(VirtualKeyCode::Left) => state.dpad_left.update(true, irq),
Some(VirtualKeyCode::Right) => state.dpad_right.update(true, irq),
Some(VirtualKeyCode::Return) => state.start.update(true, irq),
Some(VirtualKeyCode::RShift) => state.select.update(true, irq),
Some(VirtualKeyCode::Z) => state.south.update(true, irq),
Some(VirtualKeyCode::X) => state.east.update(true, irq),
None | Some(_) => {}
},
ElementState::Released => match key.virtual_keycode {
Some(VirtualKeyCode::Down) => state.dpad_down.update(false, irq),
Some(VirtualKeyCode::Up) => state.dpad_up.update(false, irq),
Some(VirtualKeyCode::Left) => state.dpad_left.update(false, irq),
Some(VirtualKeyCode::Right) => state.dpad_right.update(false, irq),
Some(VirtualKeyCode::Return) => state.start.update(false, irq),
Some(VirtualKeyCode::RShift) => state.select.update(false, irq),
Some(VirtualKeyCode::Z) => state.south.update(false, irq),
Some(VirtualKeyCode::X) => state.east.update(false, irq),
None | Some(_) => {}
},
}
}

View File

@@ -9,6 +9,7 @@ mod bus;
mod cartridge;
mod cpu;
pub mod emu;
pub mod gui;
mod high_ram;
mod instruction;
mod interrupt;

View File

@@ -1,21 +1,18 @@
use anyhow::Result;
use std::time::Instant;
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
use gb::emu::{Emulator, CYCLES_IN_FRAME};
use gb::{Cycle, GB_HEIGHT, GB_WIDTH};
use egui_wgpu_backend::RenderPass;
use gb::emu::Emulator;
use gb::gui::GuiState;
use gilrs::Gilrs;
use pixels::{PixelsBuilder, SurfaceTexture};
use rodio::{OutputStream, Sink};
use tracing_subscriber::EnvFilter;
use winit::dpi::{LogicalSize, PhysicalSize};
use winit::event::{Event, VirtualKeyCode};
use winit::event::{Event, WindowEvent};
use winit::event_loop::{ControlFlow, EventLoop};
use winit::window::{Window, WindowBuilder};
use winit_input_helper::WinitInputHelper;
const WINDOW_SCALE: usize = 3;
const AUDIO_ENABLED: bool = true;
fn main() -> Result<()> {
fn main() {
let app = App::new(crate_name!())
.version(crate_version!())
.author(crate_authors!())
@@ -48,10 +45,28 @@ fn main() -> Result<()> {
.with_env_filter(EnvFilter::from_default_env())
.init();
// --Here lies a lot of Winit + WGPU Boilerplate--
let event_loop: EventLoop<Event<()>> = EventLoop::with_user_event();
let window = gb::gui::build_window(&event_loop).expect("build window");
let (instance, surface) = gb::gui::create_surface(&window);
let adapter = gb::gui::request_adapter(&instance, &surface).expect("request adaptor");
let (device, queue) = gb::gui::request_device(&adapter).expect("request device");
let format = surface
.get_preferred_format(&adapter)
.expect("get surface format");
let mut config = gb::gui::surface_config(&window, format);
surface.configure(&device, &config);
let mut platform = gb::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 emu = match m.value_of("boot") {
Some(path) => {
tracing::info!("User-provided boot ROM");
Emulator::from_boot_rom(path)?
Emulator::from_boot_rom(path).expect("initialize emulator with custom boot rom")
}
None => {
tracing::info!("Built-in boot ROM");
@@ -59,39 +74,33 @@ fn main() -> Result<()> {
}
};
// Set up the WGPU (and then EGUI) texture we'll be working with.
let texture_size = gb::gui::texture_size();
let texture = gb::gui::create_texture(&device, texture_size);
gb::gui::write_to_texture(&queue, &texture, gb::emu::pixel_buf(&emu), texture_size);
let texture_id = gb::gui::expose_texture_to_egui(&mut render_pass, &device, &texture);
// Load ROM if filepath was provided
if let Some(path) = m.value_of("rom") {
tracing::info!("User-provided cartridge ROM");
emu.read_game_rom(path)?;
emu.read_game_rom(path).expect("read game rom from path");
}
// Load Save file if it exists
// Load Save File if it exists
// FIXME: Shouldn't the API be better than this?
emu.try_load_sav().expect("Load save if exists");
let rom_title = emu.title();
let rom_title = emu.title().to_string();
tracing::info!("Initialize Gamepad");
let mut gamepad = Gilrs::new().expect("Initialize Controller Support");
// Initialize GUI
let event_loop = EventLoop::new();
let mut input = WinitInputHelper::new();
let window = create_window(&event_loop, rom_title)?;
let mut pixels = {
let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, &window);
PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture)
.enable_vsync(false)
.build()?
};
// Initialize Audio
let (_stream, stream_handle) = OutputStream::try_default().expect("Initialized Audio");
if AUDIO_ENABLED {
let (prod, cons) = gb::spsc_init();
let sink = {
let s = Sink::try_new(&stream_handle)?;
let s = Sink::try_new(&stream_handle).expect("create sink from audio stream handle");
s.append(cons);
s.set_volume(0.1);
s
@@ -105,58 +114,85 @@ fn main() -> Result<()> {
});
}
let mut cycle_count: Cycle = Default::default();
// Set up state for the Immediate-mode GUI
let mut app = GuiState::new(rom_title);
let mut last_key = gb::gui::unused_key();
// used for egui animations
let start_time = Instant::now();
event_loop.run(move |event, _, control_flow| {
if let Event::RedrawRequested(_) = event {
if pixels.render().is_err() {
emu.try_write_sav().expect("Write game save if need be");
platform.handle_event(&event);
*control_flow = ControlFlow::Exit;
return;
}
}
match event {
Event::MainEventsCleared => {
if app.quit {
*control_flow = ControlFlow::Exit;
}
if input.update(&event) {
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
emu.try_write_sav().expect("Write game save if need be");
gb::emu::run_frame(&mut emu, &mut gamepad, last_key);
*control_flow = ControlFlow::Exit;
return;
}
if let Some(size) = input.window_resized() {
pixels.resize_surface(size.width, size.height);
}
cycle_count += gb::emu::run_frame(&mut emu, &mut gamepad, &input);
if cycle_count >= CYCLES_IN_FRAME {
cycle_count %= CYCLES_IN_FRAME;
let buf: &mut [u8; GB_WIDTH * GB_HEIGHT * 4] = pixels
.get_frame()
.try_into()
.expect("Size of Pixel Buffer is GB_WIDTH * GB_HEIGHT * 4");
gb::emu::draw_frame(&emu, buf);
window.request_redraw();
}
Event::RedrawRequested(..) => {
platform.update_time(start_time.elapsed().as_secs_f64());
let data = gb::emu::pixel_buf(&emu);
gb::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 = gb::gui::create_view(&output_frame);
// Begin to draw Egui components
platform.begin_frame();
gb::gui::draw_egui(&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 = gb::gui::create_command_encoder(&device);
let screen_descriptor = gb::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.
gb::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 {
WindowEvent::Resized(size) => {
config.width = size.width;
config.height = size.height;
surface.configure(&device, &config);
}
WindowEvent::CloseRequested => {
*control_flow = ControlFlow::Exit;
}
WindowEvent::KeyboardInput { input, .. } => last_key = input,
_ => {}
},
_ => {}
}
});
}
fn create_window(event_loop: &EventLoop<()>, title: &str) -> Result<Window> {
let logical = LogicalSize::new(GB_WIDTH as f64, GB_HEIGHT as f64);
let physical = PhysicalSize::new(
(GB_WIDTH * WINDOW_SCALE) as f32,
(GB_HEIGHT * WINDOW_SCALE) as f32,
);
Ok(WindowBuilder::new()
.with_title(title)
.with_min_inner_size(logical)
.with_inner_size(physical)
.with_resizable(true)
.build(event_loop)?)
}