This commit is contained in:
Henrik Persson 2023-01-20 08:26:46 +01:00
parent 511fbea69f
commit d328521df7
39 changed files with 758 additions and 363 deletions

4
.gitignore vendored
View file

@ -2,4 +2,6 @@
.DS_Store
/legal-roms
Cargo.lock
dhat*.json
dhat*.json
profile*.json
node_modules

View file

@ -5,9 +5,11 @@ members = [
"nes",
"nes-sdl",
"nes-wasm",
"nes-android",
"nes-android"
]
exclude = [
"nes-embedded",
"bench",
"profile"
]
[profile.test]

View file

@ -1,5 +1,4 @@
# 🥔 Potatis
<img width="400" alt="smb" src="screenshots/smb.png"><img width="400" alt="smb3" src="screenshots/smb3.png">
<img width="400" alt="bb" src="screenshots/bb.png"><img width="400" alt="dr" src="screenshots/dr.png">
@ -7,6 +6,7 @@
- `/nes` - A very incomplete NES emulator.
- `/nes-sdl` - Native target using SDL.
- `/nes-wasm` - Browser target using WASM.
- `/nes-embedded` - Embedded target for RP-2040 (Raspberry Pi Pico).
- `/nes-android` - Android target using JNI.
## /mos6502
@ -19,7 +19,7 @@ let mut machine = Mos6502::new(cpu);
loop {
machine.tick()
println!("{}", machine); // Will print nestest-like output
println!("{}", machine); // Prints nestest-like output
}
```
@ -45,7 +45,7 @@ Supported mappers:
- MMC3 (mapper 4)
```rust
impl nes::HostSystem for MyHost {
impl nes::HostPlatform for MyHost {
fn render(&mut self, frame: &RenderFrame) {
// frame.pixels() == 256 * 240 * 3 RGB array
}
@ -80,9 +80,28 @@ loop {
Try it here: https://henrikpersson.github.io/nes/index.html
## /nes-android
<img height="300" alt="bb" src="screenshots/android.png" align="right">
## /nes-embedded
It also runs on a RP-Pico with only 264kB available RAM! Without any optimizations it started out at ~0.5 FPS. But after some overclocking, and offloading the display rendering to the second CPU core, it now runs at a steady 5 FPS.
Total heap usage, single-core: 135kB
<br>
Total heap usage, multi-core: 243kB (2x frame buffers)
<img width="600" alt="smb" src="screenshots/pico.jpg">
_The second Pico on the picture is wired up as a SWD debugger/flasher. The display is a [ST7789 by Adafruit](https://www.adafruit.com/product/4311)_.
<br>
```
cd nes-embedded
ROM=/path/to/rom.nes cargo run --release
```
If you don't have a debug-probe setup, change the runner in `.cargo/config` to use a normal `elf2uf2`.
## /nes-android
1. Download Android NDK and `rustup target add [target]`
2. Configure your target(s) in `~/.cargo/config` with the linker(s) provided by the Android NDK

View file

@ -1,2 +0,0 @@
#!/bin/sh
cd bench && RUSTFLAGS=-g cargo run --release

View file

@ -1,13 +0,0 @@
[package]
name = "bench"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nes = { path = "../nes", default-features = false }
dhat = "0.3.2"
[profile.release]
debug = 1

View file

@ -1,53 +0,0 @@
#![no_std]
use nes::{cartridge::{Cartridge}, nes::Nes};
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
struct FakeHost;
impl nes::nes::HostSystem for FakeHost {
fn elapsed_millis(&self) -> usize {
0
}
fn delay(&self, _: core::time::Duration) {
}
fn render(&mut self, _: &nes::frame::RenderFrame) {
}
fn poll_events(&mut self, _: &mut nes::joypad::Joypad) -> nes::nes::Shutdown {
nes::nes::Shutdown::No
}
fn display_region(&self) -> nes::nes::HostDisplayRegion {
nes::nes::HostDisplayRegion::Ntsc
}
fn pixel_format(&self) -> nes::nes::HostPixelFormat {
nes::nes::HostPixelFormat::Rgb565
}
}
#[allow(unreachable_code)]
fn main() {
#[cfg(debug_assertions)]
panic!("run bench with --release");
let _profiler = dhat::Profiler::new_heap();
// let rom = include_bytes!("../../test-roms/nestest/nestest.nes");
let rom = include_bytes!("../../legal-roms/drmario.nes");
// let rom = include_bytes!("../../legal-roms/smb.nes");
// let rom = EmbeddedRom { full: rom };
let cart = Cartridge::blow_dust_no_heap(rom).unwrap();
let mut nes = Nes::insert(cart, FakeHost{});
for _ in 0..100000000 {
nes.tick();
}
}

View file

@ -4,6 +4,7 @@ use crate::{cpu::Cpu, memory::Bus, debugger::Debugger};
pub struct Mos6502 {
cpu: Cpu,
#[allow(dead_code)]
debugger: Debugger,
total_cycles: usize,
total_ticks: usize,

View file

@ -80,7 +80,7 @@ impl AndroidHost {
}
}
impl nes::nes::HostSystem for AndroidHost {
impl nes::nes::HostPlatform for AndroidHost {
fn elapsed_millis(&self) -> usize {
self.time.elapsed().as_millis() as usize
}
@ -90,8 +90,9 @@ impl nes::nes::HostSystem for AndroidHost {
}
fn render(&mut self, frame: &nes::frame::RenderFrame) {
let pixels: Vec<u8> = frame.pixels_ntsc().collect();
unsafe {
let jpixels: jbyteArray = self.env.byte_array_from_slice(frame.pixels()).unwrap();
let jpixels: jbyteArray = self.env.byte_array_from_slice(&pixels).unwrap();
let jobj = JObject::from_raw(jpixels);
// TODO: Is it possible/good for perf to cache the method lookup? call_method_unchecked.

View file

@ -0,0 +1,21 @@
[target.'cfg(all(target_arch = "arm", target_os = "none"))']
# Convert elf > uf2, copy to mounted pico
#runner = "elf2uf2-rs -d"
# SWD probe
runner = "probe-run --chip RP2040"
rustflags = [
"-C", "linker=flip-link",
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x", # link.x pulled from cortex-m-rt crate
"-C", "link-arg=-Tdefmt.x",
"-C", "inline-threshold=5",
"-C", "no-vectorize-loops",
]
[build]
target = "thumbv6m-none-eabi"
[env]
DEFMT_LOG = "debug"

2
nes-embedded/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
.vscode

View file

@ -3,9 +3,28 @@ name = "nes-embedded"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
embedded-alloc = "0.5.0"
rp-pico = "0.5.0" # rp-hal-boards, not needed! manually create the board here.
nes = { path = "../nes", default-features = false }
nes = { path = "../nes", default-features = false }
embedded-alloc = "0.5.0" # Heap, linked_list allocator
embedded-graphics = "0.7.1" # HAL Display drivers, drawing
embedded-hal = { version = "0.2.5", features = ["unproven"] } # The HAL. Generic pins IO etc..
cortex-m = { version = "0.7.2", features = ["critical-section"] } # low level access to cpu, delays etc.
cortex-m-rt = "0.7" # Runtime, vectors, .bss init etc.
fugit = "0.3.6" # Time, instant, delay, Hertz
rp2040-hal = { version = "0.8.0", features = ["rt", "rom-func-cache", "critical-section-impl"] }
st7789 = "0.7.0"
rp2040-boot2 = "0.2.0"
display-interface-spi = "0.4.1"
critical-section = "1.1.1"
defmt = "0.3"
defmt-rtt = "0.4"
panic-probe = { version = "0.3", features = ["print-defmt"] }
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false
incremental = false
lto = 'fat'
opt-level = 3
overflow-checks = false

18
nes-embedded/build.rs Normal file
View file

@ -0,0 +1,18 @@
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;
fn main() {
// Copy memory.x from crate-root/nes-embedded/memory.x to target output
// By default linker looks in crate-root, but that doesn't work with workspace setup
let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
File::create(out.join("memory.x"))
.expect("output path")
.write_all(include_bytes!("memory.x"))
.expect("memory.x path");
// Tell linker to look in target output
println!("cargo:rustc-link-search={}", out.display());
println!("cargo:rerun-if-changed=memory.x");
}

15
nes-embedded/memory.x Normal file
View file

@ -0,0 +1,15 @@
MEMORY
{
BOOT_LOADER : ORIGIN = 0x10000000, LENGTH = 0x100
FLASH : ORIGIN = 0x10000100, LENGTH = 2048K - 0x100
RAM : ORIGIN = 0x20000000, LENGTH = 264K
}
SECTIONS {
.boot_loader ORIGIN(BOOT_LOADER) :
{
KEEP(*(.boot_loader*));
} > BOOT_LOADER
} INSERT BEFORE .text;

104
nes-embedded/src/board.rs Normal file
View file

@ -0,0 +1,104 @@
use rp2040_hal as hal;
use hal::pac as pac;
use embedded_hal::digital::v2::OutputPin;
use hal::gpio::{Pin, PushPullOutput, FunctionSpi};
use hal::gpio::bank0::{Gpio16, Gpio17};
use hal::sio::SioGpioBank0;
use hal::{Spi, spi::Enabled};
use hal::gpio::bank0::Gpio25;
use hal::gpio::PinId;
use fugit::RateExtU32;
use display_interface_spi::SPIInterface;
use embedded_hal::{blocking::delay::DelayUs, spi::MODE_0};
use st7789::ST7789;
mod all_pins {
rp2040_hal::bsp_pins!(
Gpio16 {
name: lcd_dc,
aliases: { FunctionSpi: LcdDc }
},
Gpio17 {
name: lcd_cs,
aliases: { FunctionSpi: LcdCs }
},
Gpio18 {
name: spi_sclk,
aliases: { FunctionSpi: Sclk }
},
Gpio19 {
name: spi_mosi,
aliases: { FunctionSpi: Mosi }
},
Gpio25 { name: led },
);
}
pub type Screen = ST7789<
SPIInterface<Spi<Enabled, pac::SPI0, 8>, Pin<Gpio16, PushPullOutput>, Pin<Gpio17, PushPullOutput>>,
DummyPin,
DummyPin
>;
pub struct Pins {
pub led: Pin<Gpio25, <Gpio25 as PinId>::Reset>
}
pub struct Board {
pub screen: Screen,
}
pub struct DummyPin;
impl OutputPin for DummyPin {
type Error = ();
fn set_high(&mut self) -> Result<(), Self::Error> {
Ok(())
}
fn set_low(&mut self) -> Result<(), Self::Error> {
Ok(())
}
}
impl Board {
pub fn new(
io: pac::IO_BANK0,
pads: pac::PADS_BANK0,
sio: SioGpioBank0,
spi0: pac::SPI0,
resets: &mut pac::RESETS,
delay: &mut impl DelayUs<u32>,
) -> (Self, Pins) {
let pins = all_pins::Pins::new(io, pads, sio, resets);
let dc = pins.lcd_dc.into_push_pull_output();
let cs = pins.lcd_cs.into_push_pull_output();
let sck = pins.spi_sclk.into_mode::<FunctionSpi>();
let mosi = pins.spi_mosi.into_mode::<FunctionSpi>();
let spi_screen = Spi::<_, _, 8>::new(spi0).init(
resets,
125u32.MHz(),
16u32.MHz(),
&MODE_0
);
let spii_screen = SPIInterface::new(spi_screen, dc, cs);
let mut screen = ST7789::new(
spii_screen,
None,
None,
320,
240,
);
screen.init(delay).unwrap();
screen
.set_orientation(st7789::Orientation::LandscapeSwapped)
.unwrap();
(Self { screen }, Pins { led: pins.led })
}
}

View file

@ -0,0 +1,79 @@
use rp2040_hal as hal;
use fugit::RateExtU32;
use hal::{pac as pac, Watchdog, clocks::ClocksManager};
use hal::clocks::ClockSource;
use hal::Clock;
pub const XOSC_CRYSTAL_FREQ: u32 = 12_000_000;
pub fn configure_normal(
xosc_dev: pac::XOSC,
clocks_dev: pac::CLOCKS,
pll_sys_dev: pac::PLL_SYS,
pll_usb_dev: pac::PLL_USB,
resets: &mut pac::RESETS,
watchdog: &mut Watchdog,
) {
hal::clocks::init_clocks_and_plls(
XOSC_CRYSTAL_FREQ,
xosc_dev,
clocks_dev,
pll_sys_dev,
pll_usb_dev,
resets,
watchdog,
)
.ok()
.unwrap();
}
pub fn configure_overclock(
xosc_dev: pac::XOSC,
clocks_dev: pac::CLOCKS,
pll_sys_dev: pac::PLL_SYS,
pll_usb_dev: pac::PLL_USB,
resets: &mut pac::RESETS,
watchdog: &mut Watchdog,
) -> ClocksManager {
let xosc = hal::xosc::setup_xosc_blocking(
xosc_dev,
XOSC_CRYSTAL_FREQ.Hz()
).unwrap();
watchdog.enable_tick_generation((XOSC_CRYSTAL_FREQ / 1_000_000) as u8);
let mut clocks = ClocksManager::new(clocks_dev);
let pll_sys = hal::pll::setup_pll_blocking(
pll_sys_dev,
xosc.operating_frequency(),
hal::pll::PLLConfig {
vco_freq: fugit::HertzU32::MHz(1500),
refdiv: 1,
post_div1: 3,
post_div2: 2,
},
&mut clocks,
resets,
)
.unwrap();
let pll_usb = hal::pll::setup_pll_blocking(
pll_usb_dev,
xosc.operating_frequency(),
hal::pll::common_configs::PLL_USB_48MHZ,
&mut clocks,
resets,
)
.unwrap();
clocks
.system_clock
.configure_clock(&pll_sys, pll_sys.get_freq())
.unwrap();
clocks.init_default(&xosc, &pll_sys, &pll_usb).unwrap();
clocks
}

View file

@ -1,32 +1,163 @@
#![no_std]
#![no_main]
#![feature(alloc_error_handler)]
use pimoroni_pico_explorer::entry;
use nes::{cartridge::Cartridge, nes::Nes};
use rp_pico::entry;
use rp2040_hal as hal;
use hal::{multicore::Stack, sio::SioFifo, pac, Sio, Watchdog};
// PicoHost?
struct EmbeddedHost {
mod clocks;
mod board;
use board::Board;
use critical_section::Mutex;
use defmt::*;
use defmt_rtt as _;
use panic_probe as _;
use embedded_alloc::Heap;
use nes::{cartridge::Cartridge, nes::{Nes, HostPixelFormat}, frame::PixelFormat};
use core::{alloc::Layout, cell::RefCell};
use embedded_graphics::{
prelude::*,
pixelcolor::Rgb565, image::{ImageRaw, Image},
};
use embedded_hal::digital::v2::OutputPin;
use hal::multicore::Multicore;
use hal::clocks::Clock;
#[link_section = ".boot_loader"]
#[used]
pub static BOOT_LOADER: [u8; 256] = rp2040_boot2::BOOT_LOADER_W25Q080;
#[global_allocator]
static HEAP: Heap = Heap::empty();
const FRAME_BUF_SIZE: usize = nes::frame::NTSC_WIDTH * nes::frame::NTSC_HEIGHT * nes::frame::PixelFormatRGB565::BYTES_PER_PIXEL;
static FRAME_BUF: Mutex<RefCell<[u8; FRAME_BUF_SIZE]>> = Mutex::new(RefCell::new([0; FRAME_BUF_SIZE]));
static mut CORE1_STACK: Stack<2048> = Stack::new();
#[alloc_error_handler]
fn oom(_: Layout) -> ! {
defmt::panic!("OOM");
}
impl nes::nes::HostSystem for EmbeddedHost {
struct EmbeddedHost {
start: bool,
core1: SioFifo,
}
impl EmbeddedHost {
fn new(core1: SioFifo) -> Self {
Self {
start: false,
core1
}
}
}
impl nes::nes::HostPlatform for EmbeddedHost {
fn pixel_format(&self) -> HostPixelFormat {
HostPixelFormat::Rgb565
}
fn render(&mut self, frame: &nes::frame::RenderFrame) {
todo!()
critical_section::with(|cs| {
let mut framebuf = FRAME_BUF.borrow_ref_mut(cs);
for (i, p) in frame.pixels_ntsc().enumerate() {
framebuf[i] = p;
}
});
self.core1.write(1);
}
fn poll_events(&mut self, joypad: &mut nes::joypad::Joypad) -> nes::nes::Shutdown {
todo!()
nes::nes::Shutdown::No
}
}
#[entry]
fn main() -> ! {
let rom = include_bytes!("../../legal-roms/smb.nes");
let cart = Cartridge::load(rom).unwrap();
let nes = Nes::insert(cart, EmbeddedHost {});
fn core1_render(sys_freq: u32) -> ! {
let mut pac = unsafe { pac::Peripherals::steal() };
let core = unsafe { pac::CorePeripherals::steal() };
let mut sio = Sio::new(pac.SIO);
let mut delay = cortex_m::delay::Delay::new(core.SYST, sys_freq);
let (mut board, pins) = Board::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
pac.SPI0,
&mut pac.RESETS,
&mut delay,
);
board.screen.clear(Rgb565::BLACK).unwrap();
let mut led_pin = pins.led.into_push_pull_output();
let mut frame_n = 0;
info!("core 1 loopin");
loop {
nes.tick()
let _ = sio.fifo.read_blocking();
critical_section::with(|cs| {
let frame = FRAME_BUF.borrow(cs).borrow();
if frame_n % 2 == 0 {
led_pin.set_high().unwrap();
} else {
led_pin.set_low().unwrap();
}
let raw_image = ImageRaw::<Rgb565>::new(&frame[..], nes::frame::NTSC_WIDTH as u32);
let image = Image::new(&raw_image, Point::zero());
image.draw(&mut board.screen).unwrap();
});
frame_n += 1;
}
}
}
#[hal::entry]
fn main() -> ! {
{
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 140000;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
let mut pac = pac::Peripherals::take().unwrap();
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let clocks = clocks::configure_overclock(
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog
);
let mut sio = Sio::new(pac.SIO);
let mut mc = Multicore::new(&mut pac.PSM, &mut pac.PPB, &mut sio.fifo);
let cores = mc.cores();
let core1 = &mut cores[1];
info!("booting core1");
let sys_freq = clocks.system_clock.freq().to_Hz();
core1.spawn(unsafe { &mut CORE1_STACK.mem }, move || {
core1_render(sys_freq);
})
.expect("core1 failed");
let rom = include_bytes!(env!("ROM"));
let cart = Cartridge::blow_dust_no_heap(rom).unwrap();
let host = EmbeddedHost::new(sio.fifo);
let mut nes = Nes::insert(cart, host);
loop {
nes.tick();
}
}

View file

@ -4,7 +4,7 @@ use structopt::StructOpt;
use common::utils;
mod sdl;
use crate::sdl::SdlHostSystem;
use crate::sdl::SdlHostPlatform;
#[derive(StructOpt, Debug)]
struct Cli {
@ -26,7 +26,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let cartridge = Cartridge::blow_dust(args.path)?;
println!("Loaded! {}", cartridge);
let mut nes = Nes::insert(cartridge, SdlHostSystem::new());
let mut nes = Nes::insert(cartridge, SdlHostPlatform::new());
nes.show_fps(std::env::var("SHOW_FPS").is_ok());
let debugger = nes.debugger();

View file

@ -1,9 +1,9 @@
use std::time::Instant;
use nes::{joypad::{Joypad, JoypadEvent, JoypadButton}, frame::{RenderFrame, DisplayRegion, DisplayRegionNTSC, DisplayRegionPAL}, nes::{HostSystem, Shutdown, HostDisplayRegion}};
use nes::{joypad::{Joypad, JoypadEvent, JoypadButton}, frame::{RenderFrame}, nes::{HostPlatform, Shutdown}};
use sdl2::{pixels::PixelFormatEnum, event::Event, keyboard::Keycode, Sdl, render::{Texture, Canvas, TextureCreator}, video::{Window, WindowContext}};
pub struct SdlHostSystem<'a> {
pub struct SdlHostPlatform<'a> {
context: Sdl,
canvas: Canvas<Window>,
texture: Texture<'a>,
@ -11,12 +11,12 @@ pub struct SdlHostSystem<'a> {
time: Instant,
}
impl SdlHostSystem<'_> {
impl SdlHostPlatform<'_> {
pub fn new() -> Self {
// TODO: Inject
let scale = 4;
let w = DisplayRegionNTSC::WIDTH as u32;
let h = DisplayRegionNTSC::HEIGHT as u32;
let w = nes::frame::NTSC_WIDTH as u32;
let h = nes::frame::NTSC_HEIGHT as u32;
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
@ -49,9 +49,10 @@ impl SdlHostSystem<'_> {
}
}
impl HostSystem for SdlHostSystem<'_> {
impl HostPlatform for SdlHostPlatform<'_> {
fn render(&mut self, frame: &RenderFrame) {
self.texture.update(None, frame.pixels(), frame.pitch()).unwrap();
let pixels: Vec<u8> = frame.pixels_ntsc().collect();
self.texture.update(None, &pixels, frame.pitch_ntsc()).unwrap();
self.canvas.copy(&self.texture, None, None).unwrap();
self.canvas.present();
}

View file

@ -1,4 +1,4 @@
use nes::{cartridge::Cartridge, nes::{Nes, HostSystem, Shutdown}, joypad::{JoypadButton, JoypadEvent}, frame::{PixelFormat, SetPixel, DisplayRegionNTSC}};
use nes::{cartridge::Cartridge, nes::{Nes, HostPlatform, Shutdown}, joypad::{JoypadButton, JoypadEvent}, frame::{PixelFormat, SetPixel}};
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
pub struct PixelFormatRGBA8888;
@ -47,19 +47,21 @@ pub fn main() -> Result<(), JsValue> {
Ok(())
}
struct WasmHostSystem {
struct WasmHostPlatform {
browser: BrowserNes,
keyboard: KeyboardState,
time: wasm_timer::Instant
}
impl HostSystem for WasmHostSystem {
impl HostPlatform for WasmHostPlatform {
fn alloc_render_frame(&self) -> nes::frame::RenderFrame {
nes::frame::RenderFrame::new::<DisplayRegionNTSC, PixelFormatRGBA8888>()
nes::frame::RenderFrame::new::<PixelFormatRGBA8888>()
}
fn render(&mut self, frame: &nes::frame::RenderFrame) {
self.browser.on_frame_ready(frame.pixels().as_ptr(), frame.pixels().len());
let pixels: Vec<u8> = frame.pixels_ntsc().collect();
// assert_eq!(pixels.len(), 224 * 240 * 4);
self.browser.on_frame_ready(pixels.as_ptr(), pixels.len());
}
fn poll_events(&mut self, joypad: &mut nes::joypad::Joypad) -> Shutdown {
@ -99,7 +101,7 @@ impl HostSystem for WasmHostSystem {
}
}
impl WasmHostSystem {
impl WasmHostPlatform {
pub fn new(browser: BrowserNes) -> Self {
Self { browser, keyboard: KeyboardState::default(), time: wasm_timer::Instant::now() }
}
@ -121,7 +123,7 @@ impl NesWasm {
};
log(format!("nes init! {}", cart).as_str());
let nes = Nes::insert(cart, WasmHostSystem::new(browser));
let nes = Nes::insert(cart, WasmHostPlatform::new(browser));
Self { nes }
}

View file

@ -143,7 +143,7 @@ impl Rom for EmbeddedRom {
}
fn get(&self) -> &[u8] {
&self.0
self.0
}
}
@ -240,7 +240,7 @@ impl<R : Rom> Cartridge<R> {
Ok(Cartridge {
prg: prg_start..prg_end,
chr: chr_range,
rom: rom,
rom,
mirroring,
mapper,
format,
@ -254,12 +254,13 @@ impl<R : Rom> Cartridge<R> {
}
pub fn prg(&self) -> &[u8] {
// TODO: expensive to slice each r/w? have slice refs ready?
// TODO: Perf, expensive to slice each r/w? have slice refs ready?
&self.rom.get()[self.prg.start..self.prg.end]
}
pub fn chr(&self) -> &[u8] {
if let Some(chr_ram) = &self.chr_ram {
// TODO: Perf, get rid of this branch
if let Some(chr_ram) = &self.chr_ram {
&chr_ram[..]
} else {
&self.rom.get()[self.chr.start..self.chr.end]

View file

@ -44,79 +44,64 @@ impl SetPixel for PixelFormatRGB565 {
}
}
pub trait DisplayRegion {
const WIDTH: usize;
const HEIGHT: usize;
const OVERSCAN_PIXELS: usize;
}
// Also PAL res
pub const NES_WIDTH: usize = 256;
pub const NES_HEIGHT: usize = 240;
pub struct DisplayRegionNTSC;
impl DisplayRegion for DisplayRegionNTSC {
const WIDTH: usize = 240;
const HEIGHT: usize = 224;
const OVERSCAN_PIXELS: usize = 8;
}
pub struct DisplayRegionPAL;
impl DisplayRegion for DisplayRegionPAL {
const WIDTH: usize = 256;
const HEIGHT: usize = 240;
const OVERSCAN_PIXELS: usize = 0;
}
pub const NTSC_WIDTH: usize = 240;
pub const NTSC_HEIGHT: usize = 224;
const NTSC_OVERSCAN_PIXELS: usize = 8;
pub struct RenderFrame {
width: usize,
bytes_per_pixel: usize,
overscan_pixels: usize,
buf: Vec<u8>,
set_pixel_fn: SetPixelFn
set_pixel_fn: SetPixelFn,
pitch_ntsc: usize,
pitch_pal: usize,
}
impl RenderFrame {
pub fn new<DISPLAY, FORMAT>() -> Self
pub fn new<FORMAT>() -> Self
where
DISPLAY : DisplayRegion,
FORMAT : PixelFormat + SetPixel + 'static
{
Self {
width: DISPLAY::WIDTH,
bytes_per_pixel: FORMAT::BYTES_PER_PIXEL,
overscan_pixels: DISPLAY::OVERSCAN_PIXELS,
buf: vec![0; DISPLAY::WIDTH * DISPLAY::HEIGHT * FORMAT::BYTES_PER_PIXEL],
set_pixel_fn: FORMAT::set_pixel
buf: vec![0; NES_WIDTH * NES_HEIGHT * FORMAT::BYTES_PER_PIXEL],
set_pixel_fn: FORMAT::set_pixel,
pitch_ntsc: NTSC_WIDTH * FORMAT::BYTES_PER_PIXEL,
pitch_pal: NES_WIDTH * FORMAT::BYTES_PER_PIXEL,
}
}
fn overscan(&self, x: &mut usize, y: &mut usize) {
// 0 == no draw
// width - 8 == no draw
// 8 == 0
if *x >= self.overscan_pixels {
*x -= self.overscan_pixels;
}
if *y >= self.overscan_pixels {
*y -= self.overscan_pixels;
}
// The NES PPU always generates a 256x240 pixel picture.
pub fn set_pixel_xy(&mut self, x: usize, y: usize, rgb: (u8, u8, u8)) {
let i = ((y * NES_WIDTH) + x) * self.bytes_per_pixel;
(self.set_pixel_fn)(&mut self.buf, i, rgb);
}
pub fn set_pixel_xy(&mut self, mut x: usize, mut y: usize, rgb: (u8, u8, u8)) {
self.overscan(&mut x, &mut y);
let i = ((y * self.width) + x) * self.bytes_per_pixel;
if i < self.buf.len() {
(self.set_pixel_fn)(&mut self.buf, i, rgb);
}
pub fn pixels_pal(&self) -> &[u8] {
&self.buf
}
pub fn pixels(&self) -> &[u8] {
&self.buf[..]
// This is an iter to avoid allocations
pub fn pixels_ntsc(&self) -> impl Iterator<Item = u8> + '_ {
let buf_pitch = NES_WIDTH * self.bytes_per_pixel;
let overscan_pitch = NTSC_OVERSCAN_PIXELS * self.bytes_per_pixel;
self.buf.chunks(buf_pitch) // Chunk as rows
.skip(NTSC_OVERSCAN_PIXELS) // Skip first X rows
.map(move |row| &row[overscan_pitch..buf_pitch - overscan_pitch]) // Skip col edges
.take(NTSC_HEIGHT)
.flatten()
.copied()
}
pub fn pitch(&self) -> usize {
self.width * self.bytes_per_pixel
pub fn pitch_ntsc(&self) -> usize {
self.pitch_ntsc
}
pub fn pitch_pal(&self) -> usize {
self.pitch_pal
}
}

View file

@ -14,6 +14,8 @@ pub(crate) struct CNROM<R : Rom> {
is_16kb: bool,
}
impl<R : Rom> Mapper for CNROM<R> {}
impl<R: Rom> CNROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let is_16kb = match cart.prg().len() {
@ -30,12 +32,6 @@ impl<R: Rom> CNROM<R> {
}
}
impl<R : Rom> Mapper for CNROM<R> {
fn mirroring(&self) -> crate::cartridge::Mirroring {
self.cart.mirroring()
}
}
impl<R : Rom> Bus for CNROM<R> {
fn read8(&self, address: u16) -> u8 {
match address {

View file

@ -46,6 +46,8 @@ pub struct MMC1<R : Rom> {
shift_register: u8,
}
impl<R : Rom> Mapper for MMC1<R> {}
impl<R : Rom> MMC1<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let mirroring = cart.mirroring();
@ -178,12 +180,6 @@ impl<R : Rom> MMC1<R> {
}
}
impl<R : Rom> Mapper for MMC1<R> {
fn mirroring(&self) -> crate::cartridge::Mirroring {
self.mirroring
}
}
impl <R : Rom>Bus for MMC1<R> {
fn read8(&self, address: u16) -> u8 {
// println!("Read: {:#06x}", address);

View file

@ -1,7 +1,7 @@
use core::panic;
use common::kilobytes;
use mos6502::memory::Bus;
use alloc::boxed::Box;
use crate::cartridge::{Cartridge, Mirroring, Rom};
use super::Mapper;
@ -25,7 +25,7 @@ pub struct MMC3<R : Rom> {
prg_rom_bank_mode: PrgBankMode,
chr_rom_bank_mode: ChrBankMode,
mirroring: Option<Mirroring>,
mirroring_cb: Option<Box<dyn FnMut(&Mirroring)>>,
registers: [u8; 8],
register_to_update: u8, // 3 bits
@ -43,7 +43,7 @@ impl<R : Rom> MMC3<R> {
cart,
prg_rom_bank_mode: PrgBankMode::Swap8000FixC000_0,
chr_rom_bank_mode: ChrBankMode::TwoKbAt0000_0,
mirroring: None,
mirroring_cb: None,
registers: [0; 8],
register_to_update: 0,
irq_enabled: false,
@ -106,13 +106,8 @@ impl<R : Rom> MMC3<R> {
}
impl<R : Rom> Mapper for MMC3<R> {
fn mirroring(&self) -> crate::cartridge::Mirroring {
// This bit has no effect on cartridges with hardwired 4-screen VRAM.
// In the iNES and NES 2.0 formats, this can be identified through bit 3 of byte $06 of the header.
if self.cart.mirroring() == Mirroring::HardwiredFourScreen || self.mirroring.is_none() {
return self.cart.mirroring()
}
self.mirroring.unwrap()
fn on_runtime_mirroring(&mut self, cb: Box<dyn FnMut(&Mirroring)>) {
self.mirroring_cb = Some(cb);
}
fn irq(&mut self) -> bool {
@ -171,8 +166,18 @@ impl<R : Rom> Bus for MMC3<R> {
}
0xa000..=0xbfff => {
if even {
// Mirroring
self.mirroring = if val & 1 == 1 { Some(Mirroring::Horizontal) } else { Some(Mirroring::Vertical) };
let runtime_mirroring = if val & 1 == 1 {
Mirroring::Horizontal
} else {
Mirroring::Vertical
};
if self.cart.mirroring() != runtime_mirroring {
let cb = self.mirroring_cb
.as_mut()
.expect("mirroring changed, no one to tell");
(*cb)(&runtime_mirroring)
}
}
// Odd: PRG RAM protect
}

View file

@ -1,7 +1,7 @@
use core::cell::RefCell;
use alloc::rc::Rc;
use mos6502::memory::Bus;
use alloc::boxed::Box;
use crate::cartridge::{Cartridge, Mirroring, Rom};
mod mmc1;
@ -11,7 +11,7 @@ mod mmc3;
mod uxrom;
pub trait Mapper : Bus {
fn mirroring(&self) -> Mirroring;
fn on_runtime_mirroring(&mut self, _: Box<dyn FnMut(&Mirroring)>) {}
fn irq(&mut self) -> bool { false }
}

View file

@ -12,6 +12,8 @@ pub struct NROM<R : Rom> {
is_16kb: bool
}
impl<R : Rom> Mapper for NROM<R> {}
impl<R : Rom> NROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
let is_16kb = match cart.prg().len() {
@ -26,12 +28,6 @@ impl<R : Rom> NROM<R> {
}
}
impl<R : Rom> Mapper for NROM<R> {
fn mirroring(&self) -> crate::cartridge::Mirroring {
self.cart.mirroring()
}
}
impl<R : Rom> Bus for NROM<R> {
fn read8(&self, address: u16) -> u8 {
match address {

View file

@ -11,6 +11,8 @@ pub struct UxROM<R : Rom> {
num_banks: usize,
}
impl<R : Rom> Mapper for UxROM<R> {}
impl<R : Rom> UxROM<R> {
pub fn new(cart: Cartridge<R>) -> Self {
Self {
@ -21,12 +23,6 @@ impl<R : Rom> UxROM<R> {
}
}
impl<R : Rom> Mapper for UxROM<R> {
fn mirroring(&self) -> crate::cartridge::Mirroring {
self.cart.mirroring()
}
}
impl<R : Rom> Bus for UxROM<R> {
fn read8(&self, address: u16) -> u8 {
let address = address as usize;

View file

@ -1,7 +1,8 @@
use core::{cell::RefCell, time::Duration};
use alloc::{rc::Rc, boxed::Box, string::ToString};
#[allow(unused_imports)]
use mos6502::{mos6502::Mos6502, memory::Bus, cpu::{Cpu, Reg}, debugger::Debugger};
use crate::{cartridge::{Cartridge, Rom}, nesbus::NesBus, ppu::ppu::{Ppu, TickEvent}, joypad::Joypad, frame::{RenderFrame, PixelFormatRGB888, DisplayRegionNTSC, PixelFormatRGB565, DisplayRegionPAL}, trace, fonts};
use crate::{cartridge::{Cartridge, Rom}, nesbus::NesBus, ppu::ppu::{Ppu, TickEvent}, joypad::Joypad, frame::{RenderFrame, PixelFormatRGB888, PixelFormatRGB565}, trace, fonts};
const DEFAULT_FPS_MAX: usize = 60;
@ -14,49 +15,35 @@ impl From<bool> for Shutdown {
}
}
#[derive(PartialEq, Default)]
pub enum HostDisplayRegion { #[default] Ntsc, Pal }
#[derive(PartialEq, Default)]
pub enum HostPixelFormat { #[default] Rgb888, Rgb565 }
pub trait HostSystem {
pub trait HostPlatform {
fn render(&mut self, frame: &RenderFrame);
fn poll_events(&mut self, joypad: &mut Joypad) -> Shutdown;
fn elapsed_millis(&self) -> usize {
// Not required. Up to platform to implement for FPS control.
return 0;
0
}
fn delay(&self, _: Duration) {
// Not required. Up to platform to implement for FPS control.
}
fn display_region(&self) -> HostDisplayRegion { HostDisplayRegion::default() }
fn pixel_format(&self) -> HostPixelFormat { HostPixelFormat::default() }
fn alloc_render_frame(&self) -> RenderFrame {
match (self.display_region(), self.pixel_format()) {
(HostDisplayRegion::Ntsc, HostPixelFormat::Rgb888) => {
RenderFrame::new::<DisplayRegionNTSC, PixelFormatRGB888>()
}
(HostDisplayRegion::Ntsc, HostPixelFormat::Rgb565) => {
RenderFrame::new::<DisplayRegionNTSC, PixelFormatRGB565>()
}
(HostDisplayRegion::Pal, HostPixelFormat::Rgb888) => {
RenderFrame::new::<DisplayRegionPAL, PixelFormatRGB888>()
}
(HostDisplayRegion::Pal, HostPixelFormat::Rgb565) => {
RenderFrame::new::<DisplayRegionPAL, PixelFormatRGB565>()
}
match self.pixel_format() {
HostPixelFormat::Rgb888 => RenderFrame::new::<PixelFormatRGB888>(),
HostPixelFormat::Rgb565 => RenderFrame::new::<PixelFormatRGB565>()
}
}
}
#[derive(Default)]
struct HeadlessHost;
impl HostSystem for HeadlessHost {
impl HostPlatform for HeadlessHost {
fn render(&mut self, _: &RenderFrame) {}
fn poll_events(&mut self, _: &mut Joypad) -> Shutdown { Shutdown::No }
fn elapsed_millis(&self) -> usize { 0 }
@ -66,7 +53,7 @@ impl HostSystem for HeadlessHost {
pub struct Nes {
machine: Mos6502,
ppu: Rc<RefCell<Ppu>>,
host: Box<dyn HostSystem>,
host: Box<dyn HostPlatform>,
joypad: Rc<RefCell<Joypad>>,
timing: FrameTiming,
show_fps: bool,
@ -74,11 +61,12 @@ pub struct Nes {
}
impl Nes {
pub fn insert<H : HostSystem + 'static, R : Rom + 'static>(cartridge: Cartridge<R>, host: H) -> Self {
pub fn insert<H : HostPlatform + 'static, R : Rom + 'static>(cartridge: Cartridge<R>, host: H) -> Self {
let mirroring = cartridge.mirroring();
let rom_mapper = crate::mappers::for_cart(cartridge);
let frame = host.alloc_render_frame();
let ppu = Rc::new(RefCell::new(Ppu::new(rom_mapper.clone(), frame)));
let ppu = Rc::new(RefCell::new(Ppu::new(rom_mapper.clone(), mirroring, frame)));
let joypad = Rc::new(RefCell::new(Joypad::default()));
let bus = NesBus::new(rom_mapper.clone(), ppu.clone(), joypad.clone());

View file

@ -90,7 +90,7 @@ impl Bus for NesBus {
_ => unreachable!()
}
}
MappedDevice::CpuTest => (), // TODO
MappedDevice::CpuTest => (),
MappedDevice::Cartridge => self.rom.borrow_mut().write8(val, address),
}
}
@ -98,16 +98,12 @@ impl Bus for NesBus {
#[cfg(test)]
mod tests {
use crate::frame::{RenderFrame, DisplayRegionNTSC, PixelFormatRGB888};
use crate::{frame::{RenderFrame, PixelFormatRGB888}, cartridge::Mirroring};
use super::*;
struct TestBus{}
impl Mapper for TestBus {
fn mirroring(&self) -> crate::cartridge::Mirroring {
todo!()
}
}
impl Mapper for TestBus {}
impl Bus for TestBus {
fn read8(&self, _: u16) -> u8 {
@ -122,10 +118,10 @@ mod tests {
fn sut() -> NesBus {
let bus = Rc::new(RefCell::new(TestBus{}));
let joypad = Joypad::default();
let frame = RenderFrame::new::<DisplayRegionNTSC, PixelFormatRGB888>();
let frame = RenderFrame::new::<PixelFormatRGB888>();
NesBus::new(
bus.clone(),
Rc::new(RefCell::new(Ppu::new(bus, frame))),
Rc::new(RefCell::new(Ppu::new(bus, Mirroring::Horizontal, frame))),
Rc::new(RefCell::new(joypad))
)
}

View file

@ -12,6 +12,74 @@ pub const BLARRG_PALETTE: [u8; PALETTE_SIZE] = [
0x00,0x20,0x2C,0x08
];
// https://www.nesdev.org/wiki/PPU_palettes
static PALETTE_RGB: [(u8, u8, u8); 64] = [
(101, 101, 101),
(0 , 45, 105),
(19, 31, 127),
(69 , 19, 124),
(96 , 11, 98),
(115, 10, 55),
(113, 15, 7),
(90 , 26, 0),
(52 , 40, 0),
(11 , 52, 0),
(0, 60, 0),
(0, 61, 16),
(0, 56, 64),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(174,174 ,174),
(15 , 99,179),
(64 , 81, 208),
(120, 65, 204),
(167, 54, 169),
(192, 52, 112),
(189, 60, 48),
(159, 74, 0),
(109, 92, 0),
(54 , 109 , 0),
(7 , 119 , 4),
(0 , 121 , 61),
(0, 114 ,125),
(0, 0, 0),
(0, 0, 0),
(0, 0, 0),
(254, 254, 255),
(93, 179, 255),
(143, 161, 255),
(200, 144, 255),
(247, 133, 250),
(255, 131, 192),
(255, 139, 127),
(239, 154, 73),
(189, 172, 44),
(133, 188, 47),
(85, 199, 83),
(60, 201, 140),
(62, 194, 205),
(78, 78, 78),
(0, 0, 0),
(0, 0, 0),
(254, 254, 255),
(188, 223, 255),
(209, 216, 255),
(232, 209, 255),
(251, 205, 253),
(255, 204, 229),
(255, 207, 202),
(248, 213, 180),
(228, 220, 168),
(204, 227, 169),
(185, 232, 184),
(174, 232, 208),
(175, 229, 234),
(182, 182, 182),
(0, 0, 0),
(0, 0, 0),
];
pub struct Palette {
data: [u8; PALETTE_SIZE]
}
@ -34,7 +102,7 @@ impl Palette {
}
pub fn rgb_from_index(&self, index: u8) -> (u8, u8, u8) {
palette_to_rgb(self.data[index as usize])
PALETTE_RGB[self.data[index as usize] as usize % 64]
}
// 0x3f00..=0x3fff
@ -75,78 +143,4 @@ mod tests {
assert_eq!(Palette::mirror(0x3f18), 0x3f08);
assert_eq!(Palette::mirror(0x3f1c), 0x3f0c);
}
}
// https://www.nesdev.org/wiki/PPU_palettes
pub fn palette_to_rgb(value: u8) -> (u8, u8, u8) {
match value {
0x00 => (101, 101, 101),
0x01 => (0 , 45, 105),
0x02 => (19, 31, 127),
0x03 => (69 , 19, 124),
0x04 => (96 , 11, 98),
0x05 => (115, 10, 55),
0x06 => (113, 15, 7),
0x07 => (90 , 26, 0),
0x08 => (52 , 40, 0),
0x09 => (11 , 52, 0),
0x0a => (0, 60, 0),
0x0b => (0, 61, 16),
0x0c => (0, 56, 64),
0x0d => (0, 0, 0),
0x0e => (0, 0, 0),
0x0f => (0, 0, 0),
0x10 => (174,174 ,174),
0x11 => (15 , 99,179),
0x12 => (64 , 81, 208),
0x13 => (120, 65, 204),
0x14 => (167, 54, 169),
0x15 => (192, 52, 112),
0x16 => (189, 60, 48),
0x17 => (159, 74, 0),
0x18 => (109, 92, 0),
0x19 => (54 , 109 , 0),
0x1a => (7 , 119 , 4),
0x1b => (0 , 121 , 61),
0x1c => (0, 114 ,125),
0x1d => (0, 0, 0),
0x1e => (0, 0, 0),
0x1f => (0, 0, 0),
0x20 => (254, 254, 255),
0x21 => (93, 179, 255),
0x22 => (143, 161, 255),
0x23 => (200, 144, 255),
0x24 => (247, 133, 250),
0x25 => (255, 131, 192),
0x26 => (255, 139, 127),
0x27 => (239, 154, 73),
0x28 => (189, 172, 44),
0x29 => (133, 188, 47),
0x2a => (85, 199, 83),
0x2b => (60, 201, 140),
0x2c => (62, 194, 205),
0x2d => (78, 78, 78),
0x2e => (0, 0, 0),
0x2f => (0, 0, 0),
0x30 => (254, 254, 255),
0x31 => (188, 223, 255),
0x32 => (209, 216, 255),
0x33 => (232, 209, 255),
0x34 => (251, 205, 253),
0x35 => (255, 204, 229),
0x36 => (255, 207, 202),
0x37 => (248, 213, 180),
0x38 => (228, 220, 168),
0x39 => (204, 227, 169),
0x3a => (185, 232, 184),
0x3b => (174, 232, 208),
0x3c => (175, 229, 234),
0x3d => (182, 182, 182),
0x3e => (0, 0, 0),
0x3f => (0, 0, 0),
_ => {
// println!("eh");
(0, 0, 0)
}
}
}

View file

@ -1,7 +1,8 @@
use core::cell::RefCell;
use alloc::rc::Rc;
use crate::{frame::RenderFrame, trace, ppu::state::{Phase, Rendering}, mappers::Mapper};
use alloc::vec::Vec;
use crate::{frame::RenderFrame, trace, ppu::state::{Phase, Rendering}, mappers::Mapper, cartridge::Mirroring};
use super::{palette::Palette, vram::Vram, state::State};
#[derive(Default, Clone, Copy)]
@ -46,7 +47,7 @@ pub struct Ppu {
oam: [u8; 256],
oam_address: u8,
sprites: [Option<Sprite>; 8], // AKA secondary OAM
sprites: Vec<Sprite>, // AKA secondary OAM
v: u16, // Current VRAM address (15 bits)
t: u16, // Temporary VRAM address (15 bits); can also be thought of as the address of the top left onscreen tile.
@ -74,9 +75,13 @@ pub struct Ppu {
#[allow(dead_code)]
impl Ppu {
pub fn new(mapper: Rc<RefCell<dyn Mapper>>, frame: RenderFrame) -> Ppu {
pub fn new(
mapper: Rc<RefCell<dyn Mapper>>,
cart_mirroring: Mirroring,
frame: RenderFrame,
) -> Ppu {
Ppu {
vram: Vram::new(mapper.clone()),
vram: Vram::new(mapper.clone(), cart_mirroring),
rom_mapper: mapper,
palette: Palette::new(),
frame,
@ -84,7 +89,7 @@ impl Ppu {
oam: [0; 256],
oam_address: 0,
sprites: [None; 8],
sprites: Vec::with_capacity(8),
v: 0,
t: 0,
@ -340,11 +345,12 @@ impl Ppu {
fn render_sprite_pixel(&mut self, x: usize, y: usize, bg_pixel_drawn: bool) {
let x = x as u8;
let sprites_in_bounds = self.sprites.iter()
.filter_map(|s| s.as_ref())
.filter(|s| x >= s.x && x < (s.x + 8));
for sprite in self.sprites.iter() {
if x < sprite.x || x >= (sprite.x.saturating_add(8)) {
// Not in bounds
continue;
}
for sprite in sprites_in_bounds {
let pixel = 7 - (x - sprite.x);
let entry = sprite.pixels[pixel as usize];
let transparent = (entry & 0x03) == 0;
@ -367,7 +373,7 @@ impl Ppu {
fn load_sprites_for_next_scanline(&mut self) {
// Clear current sprites
self.sprites = [None; 8];
self.sprites.clear();
let sprite_height = if self.sprite_size_16 { 16 } else { 8 };
let next_line = self.state.scanline() as u8 + 1;
@ -427,7 +433,7 @@ impl Ppu {
*p = ((first_plane >> i) & 0x1) | ((second_plane >> i & 0x1) << 1) | ((attr & 0x3) << 2);
}
self.sprites[sprite_n] = Some(Sprite {
self.sprites.push(Sprite {
pixels,
priority: (attr & 0x20) == 0x20,
x,

View file

@ -63,6 +63,7 @@ impl State {
self.cycle
}
#[allow(dead_code)]
pub fn clock(&self) -> usize {
self.clock
}

View file

@ -1,28 +1,47 @@
use core::cell::RefCell;
use core::cell::{RefCell};
use alloc::rc::Rc;
use common::kilobytes;
use alloc::boxed::Box;
use crate::{cartridge::Mirroring, mappers::Mapper};
pub(crate) struct Vram {
nametables: [[u8; kilobytes::KB1]; 2], // AKA CIRAM
mapper: Rc<RefCell<dyn Mapper>>,
mirror_map: Rc<RefCell<[u8; 4]>>,
}
impl Vram {
pub fn new(mapper: Rc<RefCell<dyn Mapper>>) -> Self {
Self { nametables: [[0; kilobytes::KB1]; 2], mapper }
pub fn new(mapper: Rc<RefCell<dyn Mapper>>, cart_mirroring: Mirroring) -> Self {
let mirror_map = Self::setup_mirror_map(&cart_mirroring);
let mirror_map = Rc::new(RefCell::new(mirror_map));
let m = mirror_map.clone();
mapper.borrow_mut().on_runtime_mirroring(Box::new( move |new_mirroring| {
*m.borrow_mut() = Self::setup_mirror_map(new_mirroring);
}));
Self {
nametables: [[0; kilobytes::KB1]; 2],
mirror_map
}
}
pub fn mode(&self) -> Mirroring {
self.mapper.borrow().mirroring()
pub fn setup_mirror_map(new_mirroring: &Mirroring) -> [u8; 4] {
// https://www.nesdev.org/wiki/PPU_nametables
// https://www.nesdev.org/wiki/Mirroring#Nametable_Mirroring
match new_mirroring {
Mirroring::Vertical => [0, 1, 0, 1],
Mirroring::Horizontal => [0, 0, 1, 1],
Mirroring::SingleScreenLower => [0, 0, 0, 0],
Mirroring::SingleScreenUpper => [1, 1, 1, 1],
_ => panic!()
}
}
pub fn read(&self, address: u16) -> u8 {
assert!((0x2000..=0x2fff).contains(&address), "Invalid vram read: {:#06x}", address);
let virtual_index = Self::get_virtual_nametable_index(address);
let index = Self::map(&self.mode(), virtual_index);
let index = self.mirror_map.borrow()[virtual_index] as usize;
let offset = address & 0x3ff; // Only lower 9 bits, higher are indexing
self.nametables[index][offset as usize]
}
@ -31,44 +50,20 @@ impl Vram {
assert!((0x2000..=0x2fff).contains(&address), "Invalid vram write: {:#06x}", address);
let virtual_index = Self::get_virtual_nametable_index(address);
let index = Self::map(&self.mode(), virtual_index);
let index = self.mirror_map.borrow()[virtual_index] as usize;
let offset = address & 0x3ff; // Only lower 9 bits, higher are indexing
self.nametables[index][offset as usize] = val;
}
pub fn read_indexed(&self, virtual_index: u16, offset: u16) -> u8 {
let index = Self::map(&self.mode(), virtual_index);
let index = self.mirror_map.borrow()[virtual_index as usize] as usize;
self.nametables[index][offset as usize]
}
fn get_virtual_nametable_index(address: u16) -> u16 {
fn get_virtual_nametable_index(address: u16) -> usize {
// (0 = $2000; 1 = $2400; 2 = $2800; 3 = $2C00)
// Start == 0x2000, bit 11 & 10 selects the nametable index.
(address >> 10) & 0b11
}
// TODO: This mapping could probably be setup in the constructor already,
// instead of calculated for EVERY read/write..
fn map(mode: &Mirroring, virtual_index: u16) -> usize {
assert!(virtual_index <= 3);
// https://www.nesdev.org/wiki/PPU_nametables
// https://www.nesdev.org/wiki/Mirroring#Nametable_Mirroring
let physical_index = match (mode, virtual_index) {
(Mirroring::Vertical, 0) => 0,
(Mirroring::Vertical, 1) => 1,
(Mirroring::Vertical, 2) => 0,
(Mirroring::Vertical, 3) => 1,
(Mirroring::Horizontal, 0) => 0,
(Mirroring::Horizontal, 1) => 0,
(Mirroring::Horizontal, 2) => 1,
(Mirroring::Horizontal, 3) => 1,
(Mirroring::SingleScreenLower, _) => 0,
(Mirroring::SingleScreenUpper, _) => 1,
_ => panic!("nametable mirroring? {} {:?}", virtual_index, mode),
};
physical_index
((address >> 10) & 0b11) as usize
}
}

View file

@ -15,7 +15,7 @@ const NESTEST_SUCCESS: u16 = 0xc68b; // Here it starts writing to APU, which is
const NESTEST_RES_BYTE2: u16 = 0x0002;
const NESTEST_RES_BYTE3: u16 = 0x0003;
const ENABLE_TEST_CYCLES: bool = true;
const ENABLE_TEST_CYCLES: bool = false;
#[test]
fn nestest() {

24
profile/Cargo.toml Normal file
View file

@ -0,0 +1,24 @@
[package]
name = "profile"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
nes = { path = "../nes", default-features = false }
#linked_list_allocator = { version = "0.10.4", optional = true }
dhat = { version = "0.3.2", optional = true }
[features]
profile_heap = ["dhat"]
profile_cpu_no_std = ["profile_heap"]
profile_cpu_std = ["nes/default"]
default = []
[profile.release]
debug = 1
panic = "abort"
[profile.dev]
panic = "abort"

59
profile/src/main.rs Normal file
View file

@ -0,0 +1,59 @@
#![cfg_attr(not(feature = "profile_cpu_std"), no_std)]
// #![feature(alloc_error_handler)]
#![feature(iter_collect_into)]
// #![feature(default_alloc_error_handler)]
extern crate alloc;
use alloc::vec::Vec;
use nes::{cartridge::Cartridge, nes::Nes};
#[cfg(feature = "profile_heap")]
#[global_allocator]
static ALLOC: dhat::Alloc = dhat::Alloc;
struct FakeHost;
const EXPECTED_FRAME_SIZE: usize = 240 * 224 * 2;
impl nes::nes::HostPlatform for FakeHost {
fn elapsed_millis(&self) -> usize {
0
}
fn delay(&self, _: core::time::Duration) {
}
#[no_mangle]
fn render(&mut self, f: &nes::frame::RenderFrame) {
let mut buf = Vec::with_capacity(EXPECTED_FRAME_SIZE);
f.pixels_ntsc().collect_into(&mut buf);
assert_eq!(buf.len(), EXPECTED_FRAME_SIZE);
assert_eq!(buf.capacity(), EXPECTED_FRAME_SIZE);
}
fn poll_events(&mut self, _: &mut nes::joypad::Joypad) -> nes::nes::Shutdown {
nes::nes::Shutdown::No
}
fn pixel_format(&self) -> nes::nes::HostPixelFormat {
nes::nes::HostPixelFormat::Rgb565
}
}
#[allow(unreachable_code)]
fn main() {
#[cfg(debug_assertions)]
panic!("run profile with --release");
#[cfg(feature = "profile_heap")]
let _profiler = dhat::Profiler::new_heap();
let rom = include_bytes!(env!("PROF_ROM"));
let cart = Cartridge::blow_dust_no_heap(rom).unwrap();
let mut nes = Nes::insert(cart, FakeHost{});
for _ in 0..10_000_000 {
nes.tick();
}
}

4
profile_mem.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
cd profile &&
RUSTFLAGS=-g cargo run --features profile_heap --release &&
open 'https://nnethercote.github.io/dh_view/dh_view.html'

4
profile_perf.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
cd profile &&
RUSTFLAGS=-g cargo build --release --features profile_cpu_no_std &&
samply record ../target/release/profile

BIN
screenshots/pico.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB