broken GUI impl + Broken PPU scrolling
This commit is contained in:
@@ -38,6 +38,7 @@ impl NesBus {
|
|||||||
self.ppu.begin_frame();
|
self.ppu.begin_frame();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn controller_mut(&mut self) -> &mut Controller { &mut self.controller }
|
||||||
pub fn ppu(&self) -> &Ppu { &self.ppu }
|
pub fn ppu(&self) -> &Ppu { &self.ppu }
|
||||||
pub fn ram(&self) -> &[u8; 0x800] { &self.ram }
|
pub fn ram(&self) -> &[u8; 0x800] { &self.ram }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ pub fn key_to_button(key: Key) -> u8 {
|
|||||||
|
|
||||||
pub trait InputSource {
|
pub trait InputSource {
|
||||||
fn buttons(&mut self) -> u8;
|
fn buttons(&mut self) -> u8;
|
||||||
|
fn set_buttons(&mut self, _buttons: u8) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MaskInput {
|
pub struct MaskInput {
|
||||||
@@ -42,6 +43,7 @@ impl MaskInput {
|
|||||||
|
|
||||||
impl InputSource for MaskInput {
|
impl InputSource for MaskInput {
|
||||||
fn buttons(&mut self) -> u8 { self.state }
|
fn buttons(&mut self) -> u8 { self.state }
|
||||||
|
fn set_buttons(&mut self, buttons: u8) { self.state = buttons; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// The fixed hardware protocol - ONE implementation, any InputSource behind it.
|
// The fixed hardware protocol - ONE implementation, any InputSource behind it.
|
||||||
@@ -56,6 +58,10 @@ impl Controller {
|
|||||||
Self { source, strobe: false, shift: 0 }
|
Self { source, strobe: false, shift: 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_buttons(&mut self, buttons: u8) {
|
||||||
|
self.source.set_buttons(buttons);
|
||||||
|
}
|
||||||
|
|
||||||
// $4016/$4017 write: strobe pulse. On the falling edge, latch the buttons.
|
// $4016/$4017 write: strobe pulse. On the falling edge, latch the buttons.
|
||||||
pub fn write_strobe(&mut self, value: u8) {
|
pub fn write_strobe(&mut self, value: u8) {
|
||||||
let strobe = value & 1 == 1;
|
let strobe = value & 1 == 1;
|
||||||
|
|||||||
+60
-7
@@ -12,6 +12,8 @@ use crate::bus::{Bus, NesBus};
|
|||||||
use crate::cartridge::Cartridge;
|
use crate::cartridge::Cartridge;
|
||||||
use crate::cpu::Cpu;
|
use crate::cpu::Cpu;
|
||||||
use crate::render::{PpmRenderer, Renderer};
|
use crate::render::{PpmRenderer, Renderer};
|
||||||
|
use eframe::egui;
|
||||||
|
use crate::render::WindowRenderer;
|
||||||
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
@@ -20,8 +22,9 @@ fn main() {
|
|||||||
1 => run_nestest(),
|
1 => run_nestest(),
|
||||||
3 if args[1] == "blargg" => run_blargg(&args[2]),
|
3 if args[1] == "blargg" => run_blargg(&args[2]),
|
||||||
3 if args[1] == "dbg" => run_dbg(&args[2]),
|
3 if args[1] == "dbg" => run_dbg(&args[2]),
|
||||||
|
3 if args[1] == "gui" => run_gui(&args[2]),
|
||||||
2 => run_cart(&args[1]),
|
2 => run_cart(&args[1]),
|
||||||
_ => eprintln!("usage: rnes [<rom> | blargg <rom>]"),
|
_ => eprintln!("usage: rnes [<rom> | blargg <rom> | dbg <rom> | gui <rom>]"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,13 +37,8 @@ fn run_cart(path: &str) {
|
|||||||
let frames: u32 = 180;
|
let frames: u32 = 180;
|
||||||
|
|
||||||
for _ in 0..frames {
|
for _ in 0..frames {
|
||||||
while !bus.frame_done() {
|
run_one_frame(&mut bus, &mut cpu);
|
||||||
let cycles = cpu.step(&mut bus);
|
|
||||||
bus.tick_ppu(cycles as u64);
|
|
||||||
if bus.take_nmi() { cpu.set_nmi(); } // NMI fires when vblank hits, mid-frame
|
|
||||||
}
|
|
||||||
renderer.present(bus.framebuffer(), 256, 240);
|
renderer.present(bus.framebuffer(), 256, 240);
|
||||||
bus.begin_frame();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,3 +93,58 @@ fn run_dbg(path: &str) {
|
|||||||
}
|
}
|
||||||
println!("wrote debug_out/ + renders/, {frames} frames");
|
println!("wrote debug_out/ + renders/, {frames} frames");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_one_frame(bus: &mut NesBus, cpu: &mut Cpu) {
|
||||||
|
while !bus.frame_done() {
|
||||||
|
let cycles = cpu.step(bus);
|
||||||
|
bus.tick_ppu(cycles as u64);
|
||||||
|
if bus.take_nmi() { cpu.set_nmi(); }
|
||||||
|
}
|
||||||
|
bus.begin_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GuiApp {
|
||||||
|
bus: NesBus,
|
||||||
|
cpu: Cpu,
|
||||||
|
renderer: WindowRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GuiApp {
|
||||||
|
fn new(cc: &eframe::CreationContext<'_>, cart: Cartridge) -> Self {
|
||||||
|
let mut bus = NesBus::new(cart);
|
||||||
|
let mut cpu = Cpu::new();
|
||||||
|
cpu.reset(&mut bus);
|
||||||
|
let renderer = WindowRenderer::new(cc, 256, 240);
|
||||||
|
Self { bus, cpu, renderer }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for GuiApp {
|
||||||
|
// Non-UI work: emulation runs here, before painting.
|
||||||
|
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
self.renderer.poll_input(self.bus.controller_mut()); // keyboard -> $4016/$4017
|
||||||
|
run_one_frame(&mut self.bus, &mut self.cpu); // one emulated frame
|
||||||
|
self.renderer.present(self.bus.framebuffer(), 256, 240);
|
||||||
|
ctx.request_repaint(); // keep it running at 60fps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Painting: draw the framebuffer texture.
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
|
self.renderer.draw(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_gui(path: &str) {
|
||||||
|
let cart = Cartridge::from_file(path).unwrap();
|
||||||
|
let options = eframe::NativeOptions {
|
||||||
|
viewport: egui::ViewportBuilder::default()
|
||||||
|
.with_inner_size([1024.0, 960.0])
|
||||||
|
.with_title("rnes"),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
eframe::run_native(
|
||||||
|
"rnes",
|
||||||
|
options,
|
||||||
|
Box::new(move |cc| Ok(Box::new(GuiApp::new(cc, cart)))),
|
||||||
|
).expect("failed to run GUI");
|
||||||
|
}
|
||||||
+69
-2
@@ -1,15 +1,17 @@
|
|||||||
|
use eframe::egui;
|
||||||
|
use crate::input::{key_to_button, Controller, Key};
|
||||||
use crate::palette::index_to_rgb;
|
use crate::palette::index_to_rgb;
|
||||||
|
|
||||||
pub trait Renderer {
|
pub trait Renderer {
|
||||||
fn present(&mut self, framebuffer: &[u8], width: u16, height: u16);
|
fn present(&mut self, framebuffer: &[u8], width: u16, height: u16);
|
||||||
|
fn should_close(&self) -> bool { false } // headless never closes
|
||||||
|
fn poll_input(&mut self, _controller: &mut Controller) {} // headless: no input
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PpmRenderer {
|
pub struct PpmRenderer {
|
||||||
frame: u32,
|
frame: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
impl PpmRenderer {
|
impl PpmRenderer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { frame: 0 }
|
Self { frame: 0 }
|
||||||
@@ -26,6 +28,71 @@ impl Renderer for PpmRenderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The GUI renderer: egui window, texture upload, keyboard -> controller.
|
||||||
|
pub struct WindowRenderer {
|
||||||
|
ctx: egui::Context,
|
||||||
|
texture: Option<egui::TextureHandle>,
|
||||||
|
rgb: Vec<u8>, // reusable RGB buffer (palette indices -> RGB)
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowRenderer {
|
||||||
|
pub fn new(cc: &eframe::CreationContext<'_>, width: u16, height: u16) -> Self {
|
||||||
|
Self {
|
||||||
|
ctx: cc.egui_ctx.clone(),
|
||||||
|
texture: None,
|
||||||
|
rgb: vec![0; width as usize * height as usize * 3],
|
||||||
|
width: width as usize,
|
||||||
|
height: height as usize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from eframe::App::ui: draws the texture filling the window.
|
||||||
|
pub fn draw(&mut self, ui: &mut egui::Ui) {
|
||||||
|
if let Some(tex) = &self.texture {
|
||||||
|
let avail = ui.available_size();
|
||||||
|
ui.add(egui::Image::new(tex).fit_to_exact_size(avail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Renderer for WindowRenderer {
|
||||||
|
fn present(&mut self, framebuffer: &[u8], width: u16, height: u16) {
|
||||||
|
// palette indices -> RGB
|
||||||
|
for (i, &idx) in framebuffer.iter().enumerate() {
|
||||||
|
let (r, g, b) = index_to_rgb(idx);
|
||||||
|
self.rgb[i * 3] = r;
|
||||||
|
self.rgb[i * 3 + 1] = g;
|
||||||
|
self.rgb[i * 3 + 2] = b;
|
||||||
|
}
|
||||||
|
let image = egui::ColorImage::from_rgb([self.width, self.height], &self.rgb);
|
||||||
|
match &mut self.texture {
|
||||||
|
Some(t) => t.set(image, egui::TextureOptions::NEAREST),
|
||||||
|
None => {
|
||||||
|
let t = self.ctx.load_texture("nes-screen", image, egui::TextureOptions::NEAREST);
|
||||||
|
self.texture = Some(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_input(&mut self, controller: &mut Controller) {
|
||||||
|
let buttons = self.ctx.input(|i| {
|
||||||
|
let mut b = 0u8;
|
||||||
|
if i.key_down(egui::Key::Z) { b |= key_to_button(Key::A); }
|
||||||
|
if i.key_down(egui::Key::X) { b |= key_to_button(Key::B); }
|
||||||
|
if i.modifiers.shift { b |= key_to_button(Key::Select); }
|
||||||
|
if i.key_down(egui::Key::Enter) { b |= key_to_button(Key::Start); }
|
||||||
|
if i.key_down(egui::Key::ArrowUp) { b |= key_to_button(Key::Up); }
|
||||||
|
if i.key_down(egui::Key::ArrowDown) { b |= key_to_button(Key::Down); }
|
||||||
|
if i.key_down(egui::Key::ArrowLeft) { b |= key_to_button(Key::Left); }
|
||||||
|
if i.key_down(egui::Key::ArrowRight) { b |= key_to_button(Key::Right); }
|
||||||
|
b
|
||||||
|
});
|
||||||
|
controller.set_buttons(buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn ppm_bytes(framebuffer: &[u8], width: u16, height: u16) -> Vec<u8> {
|
pub fn ppm_bytes(framebuffer: &[u8], width: u16, height: u16) -> Vec<u8> {
|
||||||
let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
|
let mut out = format!("P6\n{width} {height}\n255\n").into_bytes();
|
||||||
for &idx in framebuffer {
|
for &idx in framebuffer {
|
||||||
|
|||||||
Reference in New Issue
Block a user