10 ms·
Implementing a NES Emulator in Rust
- sidcool 8y agoQuestion, what made it easier to build this in Rust than other languages? I know the advantages of Rust, but what exactly was the experience in this context?
- deleted 8y ago[deleted]
- joshbaptiste 8y agoImplementing an NES emulator is a great way to learn more about a language and the internals of the NES (which is well documented). It's sorta like a rite of passage and has been done in most of the "newer" languages like Nim (https://github.com/def-/nimes https://github.com/def-/nimes) , Go (https://github.com/fogleman/nes https://github.com/fogleman/nes)
- ofrzeta 8y agoIt's actually "rite of passage" as in latin "ritus".
- joshbaptiste 8y agooops fixed.. thx
- primo44 8y agoAlmost: rite _of_ passage (not "to")
- joshbaptiste 8y agoargh.. fixed again thx
- jerf 8y agoYou have now completed this write of passage.
- oconnor663 8y agoTo become a passage wright.
- nukeop 8y agoWell-documented, but many sources are contradictory. For such well researched hardware, there is a lot to be confused about when researching this topic by yourself.
- Kye 8y agoAt least there's no shortage of implementations to reference.
- deleted 8y ago[deleted]
- FartyMcFarter 8y agoIt seems that unsafe references were used, so the advantages of Rust appear to have been somewhat relinquished in this case.
- bionicbits 8y agoI am not sure you can not use `unsafe` in an embedded type of project. `unsafe` isn't necessarily bad, but when used at least makes it very clear where extra attention should be paid.
- nukeop 8y agoI also wrote a NES emulator in Rust, and managed to do it without using a single unsafe block. I used SDL bindings for graphics, if that counts, but it can also run in headless mode (for testing) without SDL perfectly well.
- neurotrace 8y agoDo you have a link? I'd like to see how yours compares with OPs
- hopler 8y ago
- bfrydl 8y agoThe author doesn't really suggest that Rust was chosen for any particular reason. A few benefits are mentioned but they aren't really specific to emulators.
- ChrisRR 8y agoBecause why not?
- xfer 8y agoIt's probably because implementing an emulator is highly rewarding and easy enough, hence makes it more exciting during learning a new language and NES is extremely well-documented.
- hu3 8y agoFrom the article: "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."
- RealityVoid 8y agoTiny off-topic, I was at hackaday Belgrade last year and a very good friend of mine ported a NES emulator on the badge. It was amazing for me seeing him how he solved the issues so quickly and how productive he was. And it was fun fiddling with an emulator on a uncommon embedded target.
- maeln 8y agoIf you like the subject of emulator and Rust, I suggest you take a look at Ferris youtube : https://www.youtube.com/channel/UC4mpLlHn0FOekNg05yCnkzQ/videos https://www.youtube.com/channel/UC4mpLlHn0FOekNg05yCnkzQ/vid... He stream is development of a Nintendo 64 and Virtual Boy emulator in Rust.
- codetrotter 8y agoAnd here is a video [1] and free to read online book [2] that someone else made about implementing a Game Boy emulator in Rust. [1]: RustFest Rome 2018 - Ryan Levick: Oh Boy! Creating a Game Boy Emulator in Rust - https://youtu.be/B7seNuQncvU https://youtu.be/B7seNuQncvU [2]: DMG-01: How to Emulate a Game Boy - http://blog.ryanlevick.com/DMG-01/ http://blog.ryanlevick.com/DMG-01/
- jolson88 8y agoThis is amazing. I've long wanted to get into some of this stuff and lately have fallen in love with Rust. I can't wait to watch these videos :D.
- Bulbasaur2015 8y agoWhat was the most challenging part for you in building this emulator?
- MichaelBurge 8y agoIt took 14 days to get it functionally-complete, where I could beat Mario using savestates. 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.
- wiremine 8y agoThat's impressive. A few follow up questions: 1. Was this your first NES emulator? 2. Your first project in Rust?
- MichaelBurge 8y agoYes to both of these. I usually use C for projects like these, and wanted to see how Rust compared.
- codetrotter 8y agoFollow up to that: Did you write any other emulators in the past?
- cabaalis 8y agoI want to build one myself as well, but I don't want to use existing code as a guide, rather challenge myself to implement from raw specs. 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?
- toprerules 8y agoSo what are your general thoughts on Rust? Would you use it again? How does it compare to using C or C++? Did you enjoy working in it? What was disappointing/motivating about the language?
- tyingq 8y agoYou mentioned fighting the borrow checker a bit around the emulated CPU address space. Did this come up anywhere else, like frame buffers? Edit: Ugh. Never mind...not being familiar with NES I didn't recognize what a PPU was :)
- MichaelBurge 8y agoThe borrow checker is mainly a problem when the same data needs to be modified in two different places. Only the PPU writes to a frame buffer(that it owns), so it wasn't a problem there. 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.
- sidcool 8y agoWhat is something that I can do to learn Rust better? I have been through the book, but that's it. What would help me really learn Rust?
- justwalt 8y agoFor me, I’ve been rewriting my simple scripts and old school projects in Rust, which has been both enjoyable and informative. 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.
- pimeys 8y agoWhat about a command line utility to do some basic Unix tasks? Usually those are the easiest and usually also very fun things to do to learn Rust.
- wyldfire 8y ago"How do you get to Carnegie Hall?" 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.
- deleted 8y ago[deleted]
- sidcool 8y agoThat is a great analogy thanks.
- asdkhadsj 8y agoFrom personal experience, invest in good (for you) resources to learn the language. I had two attempts, and it wasn't until I took "learning" seriously and purchased a book did Rust stick. My first attempt that has worked for all other languages, mostly by just reading docs and programming, was a massive headache. 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.
- dfee 8y agoHow cool would it be if computers shipped with a small FPGA to get better performance for stuff like this?
- juliangoldsmith 8y agoDoes anyone know if OP would be able to work around the single-ownership issue using Cell/RefCell? I still haven't fully grasped how to use those, but they seem like they'd fit there.
- bfrydl 8y agoOP mentions that the use of unsafe code means it's single-threaded, so yes it's likely that RefCell could be used instead of unsafe code.
- steveklabnik 8y agoI'm frankly a bit worried about that statement; I believe it means that they're producing UB. Even with unsafe, Rust code is expected to uphold the safety rules. I haven't read the code though.
- bfrydl 8y agoOP is using unsafe to store a mut pointer to a boxed value so that it can be mutated in two places. So the statement is just referring to data races.
- steveklabnik 8y agoSounds like UB to me! I’ll have to take a look. It really depends on the details. (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...)
- MichaelBurge 8y agoSorry about that. If it helps, I'm using Rust 1.32 to build: $ 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 `
- robert-boehnke 8y agoNice, I wrote an NES emulator in Swift once: https://github.com/robb/NES https://github.com/robb/NES
- devit 8y agoWhy not use serde instead of rolling your own "Savable"? 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).
- FullyFunctional 8y agoThis is very interesting. Do you have a good example to study how this should be done?
- kouteiheika 8y agoShameless plug - for anyone that's interested, I also wrote a NES emulator in Rust a few years back: https://github.com/koute/pinky https://github.com/koute/pinky I even compiled it to WebAssembly: https://koute.github.io/pinky-web/ 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.).
- hathawsh 8y agoI'm really enjoying your 6502 emulation code. It feels clean and easy to read. https://github.com/koute/pinky/blob/master/mos6502/src/virtual_mos6502.rs https://github.com/koute/pinky/blob/master/mos6502/src/virtu... (I imagine the rest is good too, but 6502 is what I know. :-) )
- tbrock 8y agoIf I recall correctly, P.C. Walton, one of the creators of rust, used an NES emulator during rust’s initial programming to measure performance of the language’s output binaries.
- eloisius 8y agoIndeed. I peeped at his code[1] frequently while I worked on a GameBoy Original emulator[2] that I've as of yes failed to finish. [1]: https://github.com/pcwalton/sprocketnes https://github.com/pcwalton/sprocketnes [2]: https://github.com/zacstewart/gbrs https://github.com/zacstewart/gbrs
- TheServer201 8y agoIn the Iterators section if you use 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 https://godbolt.org/z/XDyx0w).
- domlebo70 8y agoAwesome. Love the blog post. I learnt Haskell using the NES (http://github.com/dbousamra/hnes http://github.com/dbousamra/hnes). Definitely recommend doing your own.
- AntiRush 8y agoThere's also sprocketnes, which was originally written ~6 years ago by pcwalton, who works on rust/servo at Mozilla. It's been kept up to date and it's interesting to see how the project evolved along with the language: https://github.com/pcwalton/sprocketnes https://github.com/pcwalton/sprocketnes
- prudhvis 8y agoWhile the project itself is a great resource, the dependencies marked as "*" are unfortunate for a project that is 6 years in the making.
- crooked-v 8y agoThis reminds me that I want to make an NES emulator in JS using Redux, in part to play around with the entire machine state being serializable/deserializable and time-travelable.
- muterad_murilax 8y agohttp://www.michaelburge.us/assets/articles/20190318-nes-emulator/bankswitch.gif http://www.michaelburge.us/assets/articles/20190318-nes-emul... Off-topic: Were the "bumping wheel" tiles ever used in the actual game? If so, in which level?
- muterad_murilax 8y agohttps://tcrf.net/Super_Mario_Bros._3/Unused_graphics#World_8 https://tcrf.net/Super_Mario_Bros._3/Unused_graphics#World_8 Apparently not.
- souprock 8y agoThis is the second emulator I'm seen in rust, the other being an ARM emulator done as a personal project by somebody who was an intern here. I do emulators, but in C. (BTW, hiring) 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.
- steveklabnik 8y agoRust does not have goto, but it’s not about being academic: a lot of Rust’s compile time checks are flow-based, and unstructured control flow would make them a lot more slow and complex. 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.
- leonardmh 8y agoI’ll upvote anything using rust in a cool way