Implementing a NES Emulator in Rust
michaelburge.us
michaelburge.us
I even compiled it to WebAssembly: https://koute.github.io/pinky-web/
> The CPU address space has several PPU registers mapped. So the CPU maintains a permanent mutable reference to the PPU. But the top-level Nes object also owns the PPU. I worked around this using Box to assign fixed memory addresses to values, and then “unsafe” pointer dereferences when needed.
There's actually a neat little trick which I used in my emulator which allows you to fulfill Rust's single ownership rule while simultaneously keeping the subcomponents of the emulator independent, making it possible to interconnect them without any unsafe and letting the compiler to optimize the whole thing as everything is statically dispatched.
Basically simplifying a little bit for each component (CPU, PPU, APU, etc.):
1. You put the whole state of the component into a separate `State` structure.
2. You create a `Context` trait which has a `get_state`/`get_state_mut` method as well as various callbacks needed by the component itself (e.g. PPU's `Context` has a `peek_video_memory` so that it can read video memory which is stored externally).
3. You create an `Interface` trait (with `Context` as supertrait) which contains the public interface of a given component (e.g. PPU has `execute`, `peek_ppustatus`, etc.).
4. You put the whole implementation of the component inside of a `Private` trait (with `Context` as supertrait).
5. You do a blanket impl of `Interface` and `Private` for any `T` which implements `Context`.
So then you create a single `Nes` structure which has `cpu::State`, `ppu::State`, etc. inside of it as fields, and implements the `cpu::Context`, `ppu::Context`, etc. traits. Inside of those impls you just wire the various subcomponents together, and voila, every component is encapsulated from each other and yet they can talk with each other, and there is no indirection anywhere (no `Box`es, no `RefCell`s, etc.).
https://github.com/koute/pinky/blob/master/mos6502/src/virtu...
(I imagine the rest is good too, but 6502 is what I know. :-) )
for (idx, x) in xs.iter().enumerate()
instead of for (x, idx) in xs.iter().zip(0..xs.len())
you get the 4x unroll (https://godbolt.org/z/XDyx0w).Also you should use Rc/Arc and Weak (along with RefCell/Mutex if needed) instead of the unsafe pointers. Even better, if possible, move the functions that access multiple components to the object that holds them all, and don't use any "smart pointers". Another possible design is to pass borrowed references explicitly to the methods, and add them to the trait signatures where needed.
Regarding memory access, it's probably fastest to have an hardcoded fast path for RAM and ROM, and then process the rest like now. Lookup tables might help as well (as long as all the data including them fits in cache, of course).
He stream is development of a Nintendo 64 and Virtual Boy emulator in Rust.
[1]: RustFest Rome 2018 - Ryan Levick: Oh Boy! Creating a Game Boy Emulator in Rust - https://youtu.be/B7seNuQncvU
[2]: DMG-01: How to Emulate a Game Boy - http://blog.ryanlevick.com/DMG-01/
In those old 8- and 16-bit machines all components usually ran completely synchronous for each clock tick, for instance if the video emulation runs out of sync with the CPU emulation for a tick or two, a write from the CPU to a video hardware register might not make it in time and you get garbage video output.
Modern asynchronous systems may need less (relative) host system performance for emulation, because the timing requirements between the different hardware components are much more relaxed.
I haven't written an emulator for the NES yet, but on machines like the Amstrad CPC or C64, the CPU can reprogram video hardware registers (like color palette entries) at any time, for instance in the middle of a scanline. When this is off by a tick, you get the color palette change a couple of pixels late or early.
If CPU and video chip emulation would run on different threads, they would need to sync with each other a few million times per second, that doesn't sound like a good idea. If synchronization is only needed once per scanline, or even per frame that's an option of course. Often this is good enough to run some games which didn't go too close to the metal (again this is only from my experience with home computer emulators, haven't done NES stuff yet).
Parallelizing through SIMD might be an option though (one can pack a lot of 4- or 8-bit counters into a 512-bit register).
Threading in that case would likely mean several orders of magnitude of performance degradation.
It's definitely possible to write an emulator without unsafe code. Here's a toy gameboy emulator I wrote a while ago in Rust without any unsafe code: https://github.com/simias/gb-rs
Here's a very incomplete PSX emulator with only a single unsafe (which might not even be necessary anymore, I haven't updated the code in a while): https://github.com/simias/rustation
Or are you saying that a dependency that I use happens to use unsafe code? In which case it's true but then by that definition it's effectively almost impossible to write 100% safe rust since the stdlib itself contains a non-negligible amount of unsafe code.
The times when unsafe is poorly used are when it’s being abused to ignore safety related compilation issues, like lifetimes and/or mutability. Though, even in those cases it might be ok if you’re using lower level concurrency primitives to enforce those guarantees.
unsafe does not remove all advantages of Rust, it only removes a few restrictions. It should be avoided unless necessary.
This matters because of stuff like the article; if you see the other thread, I’m pretty sure (though haven’t verified yet) that this code has UB, even though they’re using unsafe.
I suppose the reason I consider it restrictions is due to things like “in safe code you can not work with raw pointers”. That seems like a restriction to me, but I can see why saying that way is less accurate.
Regardless, I'm not even sure I'd say that memory safety is Rust's biggest advantage. It's certainly the headliner and it's critically important. But it's a really appealing package overall IMO.
fn reverse_slice<T>(slice: &mut [T]) {
let len = slice.len();
for i in 0..len / 2 {
// Unsafe swap to avoid the bounds check in safe swap.
unsafe {
let pa: *mut T = slice.get_unchecked_mut(i);
let pb: *mut T = slice.get_unchecked_mut(len - 1 - i);
std::ptr::swap_nonoverlapping(pa, pb);
}
}
}
In theory, I could use those unsafe pointers to alias the same value and cause UB. Or I could stash them somewhere that outlives the lifetime of what they're pointing to, and cause UB later on that way. So I have to audit the function carefully to avoid doing those things. But at the same time (in the absence of other broken unsafe code in the caller), this function can rely on a lot of guarantees:- The `slice` reference is guaranteed to be unique. No other code, on any thread, has read or write access to `slice` while this function is running.
- The length of `slice` is guaranteed to be correct. All of the memory it refers to has been properly initialized, and computing pointer offsets into it cannot overflow `usize` or otherwise cause UB. (This guarantee is surprisingly subtle, because it means the maximum length of a slice is `isize::MAX` rather than `usize::MAX`.)
- The type `T` is guaranteed to be safe to move. It doesn't contain any internal references to its own memory, which moving would invalidate.
All of this put together makes it possible to audit this function by itself, to make sure safe code can't use it to trigger UB.
"C is probably still a better choice for those cases: It’s more difficult to find someone who can implement a Rust compiler than a C compiler.
But Rust seems usable in any project where C++ is a viable language."
It's been kept up to date and it's interesting to see how the project evolved along with the language:
The PPU took 7 days, the CPU took 5 days, the APU took 1 day, and there's a spare day for everything else.
The CPU was easy to make: Its natural unit of output is a single CPU instruction. Even if there are many details, it's a straightforward process to compare an instruction-by-instruction log with a reference log until there are no differences.
The PPU was harder because the natural unit of output is an entire frame: It's not any easier to get the first pixel correct, than to get them all correct. So almost everything needs to be in place before you can validate its output.
1. Was this your first NES emulator?
2. Your first project in Rust?
The problem is I haven't really found any yet that helped me (admittedly, I have so far devoted just one Saturday morning to it.) Are there any specs about the hardware that you used, or that anyone else can share?
Everything you need is either on http://wiki.nesdev.com or on their forums. I know because that's what I did with my NES emulator - I explicitly didn't want to look at any source code and instead wanted to implement everything only based on the docs.
Granted, what's on the NESdev isn't always easy to grok, up to the point of being really confusing sometimes. What I've found really helped is gradually setting up a test suite based on various test ROMs I could find, which even allows you to implement some parts of the emulator TDD-style once you get the basics up and running.
http://tasvideos.org/EmulatorResources/NESAccuracyTests.html
For an expert performance-sensitive C programmer who pulls out all the platform-specific tricks to make stuff go fast, how do you think rust would be? The default safety is appealing, but I have lots of concerns.
Bitfield access looks painful. In C, I can set things up to make read/write named access to bitfields easy. Granted, the header to set this up is complicated: make an union of anonymous structs, each with a bitfield and the needed padding. With that though, I can use bitfields just like ordinary struct members. I can pluck fields out of opcodes for emulation, or I can fill them in for a JIT.
Sometimes in C, one might use gcc's computed goto extension. (getting a void pointer from a unary && operator applied to a label, then derefing it at the goto) It doesn't seem like rust has this, even in unsafe code. Actually, there isn't even a "goto" keyword... which I find to be a worrisome sign that might indicate stubbornly academic language design.
The bounds checking on arrays is kind of the whole point of rust, but if that can't get out of the way for speed then I'd have to make everything unsafe. If I do that, then rust is pointless. Has anybody checked the assembly to see if the compiler is good at eliminating the checks? For example, if I use a 5-bit bitfield to index into a 32-entry table, do the checks get optimized out?
What if I want aliasing? With gcc I can mark things __may_alias__, and with Visual Studio I don't even need that. I had a case where I needed to lay structs over each other like shingles, in groups of 4 with internal padding, so that the same struct member of each of the 4 structs would be adjacent in memory. This was needed so that vector intrinsics could be used.
Speaking of that, are there vector intrinsics? What if I specifically want MMX opcodes in one place (for MMX emulation) and SSE opcodes in another place?
Can I get a switch without a default, such that the compiler doesn't try to generate code for a case that isn't listed? I know this will seem like a horrible idea to many programmers, but sometimes performance matters. When the language can't keep up, I have to drop down into assembly, and that sucks more.
Bitfields have a package that makes them easy.
If you want aliasing, there’s UnsafeCell.
Yes, there are vector intrinsics, and they’re part of the language, not a non-standard extension. And there are tools to choose between things too.
Switch requires a default, but you can declare the default unreachable. You should always be able to get the same asm.
In any case, I want to be able to have both behaviors.
Edit: Ugh. Never mind...not being familiar with NES I didn't recognize what a PPU was :)
The game controllers were another case: They are mapped in CPU address space, so the CPU needs a permanent mutable reference to them. But the top-level also needs to update them with inputs read from my Xbox 360 controller.
Off-topic: Were the "bumping wheel" tiles ever used in the actual game? If so, in which level?
Apparently not.
I still haven't fully grasped how to use those, but they seem like they'd fit there.
I haven't read the code though.
(I tried to run miri on it, but I'm on Windows, and this is unix-only.)
EDIT: even with porting some of the code, I can’t get the current repository to build, there’s a borrow checker error...)
$ rustc --version
rustc 1.32.0 (9fda7c223 2019-01-16)
$ cargo --version
cargo 1.32.0 (8610973aa 2019-01-02)
$ cargo build --release
$ cargo run --release --bin nes-emulator
`1. I can send you a patch later, but if you use std::io::stdin() and stdout, instead of opening the file descriptors, it should compile on Windows.
2. The borrow error was coming from miri; it detected some UB! Compiling with cargo does work. It’s not the bit I expected; I can file a bug with the error once I’m back at my laptop.
Anyway, this is a cool project, and I’m glad you’re doing it; don’t let my worries bug you. I’ve been trying to learn more unsafe stuff and so the UB bits are just on my mind more lately.
EDIT: actually it's just from building on nightly rust, not stable. No miri needed. I wonder why stable is okay with this... anyway I filed some github issues, please feel free to ignore me or close them, but if you wanna keep talking about this, let's do that there :)
Project Euler problems are pretty trivial (wrt coding) but might have some small useful nuggets. The code advent calendar from 2018 is pretty helpful too, and it’s much more code oriented than PE problems.
I’ve seen people suggest rewriting the GNU coreutils, and I really like the idea, but not quite enough to go sifting through all of the C headers and all to get to the finish line.
Honestly, it probably doesn’t matter much what you start coding as long as you’re coding.
Seriously, the best thing to do for virtually all professional or hobby work is to get more experience with it. Choose a project with small enough scope to give you encouraging results to keep that feedback loop going.
That was years ago though, perhaps it's better now. All I know is after reading Programming Rust, I picked Rust up and it's been amazing. I love the language.