Compare commits

..

No commits in common. "d9a3a7b0dde1486efb04da9d9f7725269df6f16e" and "31abd6dc5c09e36eb3d104c0c47707f985801bbd" have entirely different histories.

5 changed files with 741 additions and 276 deletions

650
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,12 @@ anyhow = "^1.0"
bitfield = "^0.13"
clap = "^2.33"
gilrs = "^0.8"
pixels = "^0.5"
winit = "^0.25"
winit_input_helper = "^0.10"
pixels = "^0.3"
winit = "^0.24"
winit_input_helper = "^0.9"
egui = "^0.10"
egui_wgpu_backend = { git="https://github.com/hasenbanck/egui_wgpu_backend.git", rev="9d03ad345d15d1e44165849b242d3562fdf3e859" }
egui_winit_platform = { git="https://github.com/hasenbanck/egui_winit_platform.git", rev="17298250e9721e8bf2c1d4a17b3e22777f8cb2e8" }
rodio = "^0.14"
crossbeam-channel = "^0.5"

321
src/gui.rs Normal file
View File

@ -0,0 +1,321 @@
use crate::bus::BusIo;
use crate::cpu::{Cpu as SM83, Register, RegisterPair};
use egui::{ClippedMesh, FontDefinitions};
use egui_wgpu_backend::{RenderPass, ScreenDescriptor};
use egui_winit_platform::{Platform, PlatformDescriptor};
use pixels::{wgpu, PixelsContext};
use std::time::Instant;
use wgpu::TextureFormat::Bgra8UnormSrgb;
// Boilerplate code from: https://github.com/parasyte/pixels/blob/0.3.0/examples/egui-winit/src/gui.rs
/// Manages all state required for rendering egui over `Pixels`.
pub struct Egui {
// State for egui.
start_time: Instant,
platform: Platform,
screen_desc: ScreenDescriptor,
render_pass: RenderPass,
paint_jobs: Vec<ClippedMesh>,
pub config: Configuration,
show_flags: bool,
show_cpu_info: bool,
show_registers: bool,
show_int: bool,
show_timer: bool,
}
impl Egui {
/// Create egui.
pub fn new(width: u32, height: u32, scale_factor: f64, context: &PixelsContext) -> Self {
let platform = Platform::new(PlatformDescriptor {
physical_width: width,
physical_height: height,
scale_factor,
font_definitions: FontDefinitions::default(),
style: Default::default(),
});
let screen_desc = ScreenDescriptor {
physical_width: width,
physical_height: height,
scale_factor: scale_factor as f32,
};
let render_pass = RenderPass::new(&context.device, Bgra8UnormSrgb);
Self {
start_time: Instant::now(),
platform,
screen_desc,
render_pass,
paint_jobs: Vec::new(),
config: Default::default(),
show_flags: false,
show_cpu_info: false,
show_registers: false,
show_int: false,
show_timer: false,
}
}
/// Handle input events from the window manager.
pub fn handle_event(&mut self, event: &winit::event::Event<'_, ()>) {
self.platform.handle_event(event);
}
/// Resize egui.
pub fn resize(&mut self, width: u32, height: u32) {
self.screen_desc.physical_width = width;
self.screen_desc.physical_height = height;
}
/// Update scaling factor.
pub fn scale_factor(&mut self, scale_factor: f64) {
self.screen_desc.scale_factor = scale_factor as f32;
}
/// Prepare egui.
pub fn prepare(&mut self, game_boy: &SM83) {
self.platform
.update_time(self.start_time.elapsed().as_secs_f64());
// Begin the egui frame.
self.platform.begin_frame();
// Draw the demo application.
self.ui(&self.platform.context(), game_boy);
// End the egui frame and create all paint jobs to prepare for rendering.
let (_output, paint_commands) = self.platform.end_frame();
self.paint_jobs = self.platform.context().tessellate(paint_commands);
}
/// Create the UI using egui.
fn ui(&mut self, ctx: &egui::CtxRef, game_boy: &SM83) {
egui::TopPanel::top("menubar_container").show(ctx, |ui| {
egui::menu::bar(ui, |ui| {
egui::menu::menu(ui, "File", |ui| {
if ui.button("Configuration").clicked() {
self.config.show = true;
}
});
egui::menu::menu(ui, "Status", |ui| {
if ui.button("Flags").clicked() {
self.show_flags = true;
}
if ui.button("CPU Information").clicked() {
self.show_cpu_info = true;
}
if ui.button("Registers").clicked() {
self.show_registers = true;
}
if ui.button("Interrupts").clicked() {
self.show_int = true;
}
if ui.button("Timer").clicked() {
self.show_timer = true;
}
});
});
});
egui::Window::new("Cpu Flags")
.open(&mut self.show_flags)
.show(ctx, |ui| {
let flags = game_boy.flags();
ui.horizontal(|ui| {
let _ = ui.selectable_label(flags.z(), "Zero");
let _ = ui.selectable_label(flags.n(), "Negative");
let _ = ui.selectable_label(flags.h(), "Half-Carry");
let _ = ui.selectable_label(flags.c(), "Carry");
});
});
egui::Window::new("Timer")
.open(&mut self.show_timer)
.show(ctx, |ui| {
let timer = game_boy.timer();
ui.horizontal(|ui| {
ui.label("DIV");
ui.monospace(format!("{:#06X}", timer.divider));
});
ui.horizontal(|ui| {
ui.label("TIMA");
ui.monospace(format!("{:#04X}", timer.counter));
});
ui.horizontal(|ui| {
ui.label("TMA");
ui.monospace(format!("{:#04X}", timer.modulo));
});
ui.horizontal(|ui| {
ui.label("TAC");
ui.monospace(format!("{:?}", timer.ctrl));
});
});
egui::Window::new("Registers")
.open(&mut self.show_registers)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("A");
ui.monospace(format!("{:#04X}", game_boy.register(Register::A)));
ui.label("F");
ui.monospace(format!("{:#04X}", game_boy.register(Register::Flag)));
});
ui.horizontal(|ui| {
ui.label("B");
ui.monospace(format!("{:#04X}", game_boy.register(Register::B)));
ui.label("C");
ui.monospace(format!("{:#04X}", game_boy.register(Register::C)));
});
ui.horizontal(|ui| {
ui.label("D");
ui.monospace(format!("{:#04X}", game_boy.register(Register::D)));
ui.label("E");
ui.monospace(format!("{:#04X}", game_boy.register(Register::E)));
});
ui.horizontal(|ui| {
ui.label("H");
ui.monospace(format!("{:#04X}", game_boy.register(Register::H)));
ui.label("L");
ui.monospace(format!("{:#04X}", game_boy.register(Register::L)));
});
ui.horizontal(|ui| {
ui.label("AF");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::AF)));
});
ui.horizontal(|ui| {
ui.label("BC");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::BC)));
});
ui.horizontal(|ui| {
ui.label("DE");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::DE)));
});
ui.horizontal(|ui| {
ui.label("HL");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::HL)));
});
});
egui::Window::new("Cpu Information")
.open(&mut self.show_cpu_info)
.show(ctx, |ui| {
ui.horizontal(|ui| {
ui.label("PC");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::PC)));
});
ui.horizontal(|ui| {
ui.label("SP");
ui.monospace(format!("{:#06X}", game_boy.register_pair(RegisterPair::SP)));
});
ui.horizontal(|ui| {
ui.label("IME");
ui.label(format!("{:?}", game_boy.ime()));
});
ui.horizontal(|ui| {
ui.label("HALT");
ui.label(format!("{:?}", game_boy.halted()));
});
});
egui::Window::new("IRQ Information")
.open(&mut self.show_int)
.show(ctx, |ui| {
let req = game_boy.read_byte(0xFF0F);
let enabled = game_boy.read_byte(0xFFFF);
ui.heading("Interrupt Requests");
ui.horizontal(|ui| {
let _ = ui.selectable_label(req & 0x01 == 0x01, "VBLANK");
let _ = ui.selectable_label((req >> 1) & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label((req >> 2) & 0x01 == 0x01, "TIMER");
let _ = ui.selectable_label((req >> 3) & 0x01 == 0x01, "SERIAL");
let _ = ui.selectable_label((req >> 4) & 0x01 == 0x01, "JOYPAD");
});
ui.heading("Interrupt Enable");
ui.horizontal(|ui| {
let _ = ui.selectable_label(enabled & 0x01 == 0x01, "VBLANK");
let _ = ui.selectable_label((enabled >> 1) & 0x01 == 0x01, "LCD STAT");
let _ = ui.selectable_label((enabled >> 2) & 0x01 == 0x01, "TIMER");
let _ = ui.selectable_label((enabled >> 3) & 0x01 == 0x01, "SERIAL");
let _ = ui.selectable_label((enabled >> 4) & 0x01 == 0x01, "JOYPAD");
});
});
egui::Window::new("Configuration")
.open(&mut self.config.show)
.show(ctx, |_ui| {});
}
/// Render egui.
pub fn render(
&mut self,
encoder: &mut wgpu::CommandEncoder,
render_target: &wgpu::TextureView,
context: &PixelsContext,
) {
// Upload all resources to the GPU.
self.render_pass.update_texture(
&context.device,
&context.queue,
&self.platform.context().texture(),
);
self.render_pass
.update_user_textures(&context.device, &context.queue);
self.render_pass.update_buffers(
&context.device,
&context.queue,
&self.paint_jobs,
&self.screen_desc,
);
// Record all render passes.
self.render_pass.execute(
encoder,
render_target,
&self.paint_jobs,
&self.screen_desc,
None,
);
}
}
pub struct Configuration {
/// Show Configuration egui menu
show: bool,
}
impl Default for Configuration {
fn default() -> Self {
Self { show: false }
}
}

View File

@ -1,4 +1,5 @@
pub use apu::gen::AudioMPSC;
pub use gui::Egui;
pub use instruction::Cycle;
pub const GB_WIDTH: usize = 160;
@ -9,6 +10,7 @@ mod bus;
mod cartridge;
mod cpu;
pub mod emu;
mod gui;
mod high_ram;
mod instruction;
mod interrupt;

View File

@ -1,6 +1,6 @@
use anyhow::{anyhow, Result};
use clap::{crate_authors, crate_description, crate_name, crate_version, App, Arg};
use gb::{AudioMPSC, Cycle, GB_HEIGHT, GB_WIDTH};
use gb::{AudioMPSC, Cycle, Egui, GB_HEIGHT, GB_WIDTH};
use gilrs::Gilrs;
use pixels::{PixelsBuilder, SurfaceTexture};
use rodio::OutputStream;
@ -56,13 +56,17 @@ fn main() -> Result<()> {
let mut input = WinitInputHelper::new();
let window = create_window(&event_loop, cartridge_title)?;
let mut pixels = {
let (mut pixels, mut egui) = {
let size = window.inner_size();
let scale_factor = window.scale_factor();
let surface_texture = SurfaceTexture::new(size.width, size.height, &window);
PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture)
let pixels = PixelsBuilder::new(GB_WIDTH as u32, GB_HEIGHT as u32, surface_texture)
.enable_vsync(false)
.build()?
.build()?;
let egui = Egui::new(size.width, size.height, scale_factor, pixels.context());
(pixels, egui)
};
let (send, recv) = AudioMPSC::init();
@ -81,9 +85,23 @@ fn main() -> Result<()> {
let mut cycle_count: Cycle = Default::default();
event_loop.run(move |event, _, control_flow| {
// Update egui
egui.handle_event(&event);
if let Event::RedrawRequested(_) = event {
if pixels
.render()
// Prepare egui
egui.prepare(&game_boy);
// Render everything together
let render_result = pixels.render_with(|encoder, target, ctx| {
// Render the texture
ctx.scaling_renderer.render(encoder, target);
// Render egui
egui.render(encoder, target, ctx);
});
if render_result
.map_err(|e| anyhow!("pixels.render() failed: {}", e))
.is_err()
{
@ -98,8 +116,13 @@ fn main() -> Result<()> {
return;
}
if let Some(scale_factor) = input.scale_factor() {
egui.scale_factor(scale_factor);
}
if let Some(size) = input.window_resized() {
pixels.resize_surface(size.width, size.height);
egui.resize(size.width, size.height);
}
let delta = now.elapsed().subsec_nanos();