Live data from Hacker News

Implementing a NES Emulator in Rust

michaelburge.us

81–90 of 93 posts

Re: Implementing a NES Emulator in Rust

#82

What was the most challenging part for you in building this emulator?

I 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?

Shoutout to TASVideos here, who maintain a fairly up to date list of test ROMs for various hardware features. Once you have the emulator basics down (working well enough, say, to run Super Mario Bros, which is surprisingly demanding) you can throw blargg's tests at your emulator and start chasing the accuracy rabbit down the most marvelous of rabbit holes.

http://tasvideos.org/EmulatorResources/NESAccuracyTests.html

Re: Implementing a NES Emulator in Rust

#83
post #2

Question, 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?

It seems that unsafe references were used, so the advantages of Rust appear to have been somewhat relinquished in this case.

If you use unsafe Rust, you do give up the advantage of being able to say, "Look at this code, no unsafe, therefore no UB." However, the lifetime and ownership model is still doing a lot for you. You get a lot of control over the way that your safe code (hopefully most of your program) can interact with your unsafe code. For example if I have this function (partially copied from listd):

    fn reverse_slice(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.

Re: Implementing a NES Emulator in Rust

#84
This 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.

Re: Implementing a NES Emulator in Rust

#85

This 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 pain…

Rust 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.

Re: Implementing a NES Emulator in Rust

#86
post #66

If 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.

Indeed. 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

[2]: https://github.com/zacstewart/gbrs

Re: Implementing a NES Emulator in Rust

#87
post #55

Why 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 si…

This is very interesting. Do you have a good example to study how this should be done?

Re: Implementing a NES Emulator in Rust

#88

This 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 pain…

Rust has no strict aliasing, so all Rust types are may_alias.

Re: Implementing a NES Emulator in Rust

#89

Earlier quoted context omitted.

Nitpicking a bit, but there's not a lot of useful things that could be multithreaded in an 8-bit emulator, so it would be more like 1x3Ghz ;) 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…

The original hardware was multithreaded... in the sense that there were several separate processing units.

Yes, but their emulation needs to be performed in lockstep, normally per clock tick or at least externally visible state change, to accurately model their interaction with respect to timing.

Threading in that case would likely mean several orders of magnitude of performance degradation.

Re: Implementing a NES Emulator in Rust

#90
post #77
post #53

Earlier quoted context omitted.

Writing an emulator is not really embedded programming (the target often qualifies as an embedded environment but the host can be any random desktop computer). 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 n…

Are you sure about that? https://github.com/simias/gb-rs/blob/master/Cargo.toml#L29

The SDL2 dependency? I don't understand what you mean by that.

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.

Post reply on HN