Nestur: NES Emulator in Rust
github.com
github.com
If you look into speedruns you'll see that pipe behavior is very strange. One of the top speedruns involves pushing Mario into blocks in order to move the camera by something like 10 px, so that when you go down a certain pipe it goes to the warp zone instead of the bonus area.
Might be worth watching some videos about that speedrun and trying to trace what's going on in the code to allow that. It might get you closer to understanding your own pipe issue.
Unfortunately I don't have a link offhand, but search something like `perfect mario speedrun` and you'll probably find it.
Here is the video: https://youtu.be/i1AHCaokqhg
I brought it up because I'm told that the general idea applies to all pipes. They take you to the secret area for that level, and if there are multiple pipes that lead to different areas then that secret area has to be switched out. That sounds like it's relevant if the secret area in this case is an empty space, perhaps that memory hasn't been initialized or there's a specific load instruction that's doing something different in the emulator as compared to the hardware. (And for all I know the emulator could be following the spec correctly, but the hardware worked differently)
Either way, thank you for the video link.
> there are several bugs that need to be reproduced such as the oamaddr bug for example
Given what I've heard about how pipes work, that sounds like a good place to start digging in. Do you have any specific references for that? I'd much rather hear first-hand experience rather than google results, since so much of early console hardware documentation is flat out wrong.
http://wiki.nesdev.com/w/index.php/PPU_sprite_evaluation
Edit: There's also a bit of documentation in one of the bizhawk nes cores: https://github.com/TASVideos/BizHawk/tree/master/BizHawk.Emu...
Normally, Mario is locked to a maximum screen offset to the right (120 pixels, if I recall) and the table is written with the expectation that Mario will be in bounds when entering the warp; however, there are certain things (bumping things, etc) that that cause Mario to gain a few pixels past the limit. The warp will go to the current hot entry, which is the prior entry to the normal one.
The notorious use is 4-2. You want to use the vine to warp to level 8, but the vine has a huge animation time. There is a warp pipe farther past. It turns out that it's faster to time a series of bumps to get yourself several pixels past the screen limit and use the pipe. The game uses the vine's warp from the table, and now you can continue on without having gone through the vine animation.
So, the emulator may not representing the screen's offset correctly.
> So, the emulator may not representing the screen's offset correctly.
Yeah, that or the table's not being filled correctly, or it uses a load instruction that does pointer arithmetic, or something. That's my underinformed theory at least. Probably worth checking out.
https://gist.github.com/1wErt3r/4048722
At some point during every frame, shortly after the NMI routine completes, the game ends up on this particular line of code:
https://gist.github.com/1wErt3r/4048722#file-smbdis-asm-L929
This calculates an address based on the current game mode, and then jumps there. When the player performs any action that changes the level, this mode cycles a few times while the game code blanks the screen, switches gears, and loads in new data. The indirect jump and branching logic that drive the game's mode switching are tricky to get right, and if they're wrong, this little kernel will break in surprising ways.
When I was troubleshooting my branching routines, I had my emulator pause on SMB's first indirect jump, then found that position in the disassembled code and followed along. I mean, you can achieve similar results by unit testing or working to pass all of blargg's tests, but I found it much more fun to trace the emulator in a live environment, running "production" code.
I think we're on the same page, except that you sound like you actually know what you're talking about and I'm just following my gut, also known as `guessing`.
Thanks for the link, that's a great reference!
You've lost a nickel! It was a problem with my signed-offset branch function where a bad cast was allowing 0x80 (-128) to overflow. It was really fun trying to troubleshoot by hooking certain memory accesses and subroutines, but what wound up fixing it was investigating the crash of Blargg's `instr_test-v5/rom_singles/11-stack.nes`. Thank you for your help!
The core of what I wanted to point out is the behavior of warp pipes in SMB1, which was outlined quite well here: https://news.ycombinator.com/item?id=21976073
Specifically the part where there's a section in memory somewhere that contains the details of where to warp to, and that varies on Mario's x coordinate.
If a warp pipe in this emulator drops you into an empty level, that sounds to me like that bit of memory isn't getting filled, or there's a jump or load instruction that's supposed to do arithmetic/read args from a register/etc.. and it's not implemented correctly.
Also, a nitpick:
> One line of unsafe (std::mem::transmute::<u8>() -> i8)
You don't need unsafe for this, you can do that with an `as` cast.
Edit: Jesus kids, learn a little chill it was a joke. Someone didn’t like this so much they went through and downvoted all my other posts they could. Wow... that’s real serious.
Edit: yeah, “Casting between two integers of the same size (e.g. i32 -> u32) is a no-op” (https://doc.rust-lang.org/reference/expressions/operator-exp...)
In the Rust definition of safety (mutable xor shared, no data races, memory safety, etc), treating the bits of a u8 as an i8 is safe and can be done with an `as` cast.
Kudos to getting to this point. Adding support for more mappers is a nice incremental thing that can be done at your leisure, and once you get save states implemented, playing games at your own pace is super fun :)
SBC = A - V - (1 - C)
SBC = A + (-1 * (V - (1 - C)))
SBC = A + (-V + (1 - C))
SBC = A + -V + 1 + C
I am not able to follow the mathemagic behind the third line. Is it normal algebra or its in 1's complement?
SBC = A - V - 1 + C
Thanks. I'll fix this up :)
Also, this line is bothersome, they do opposite things but that's why you have already done (1 - C).
Maybe the fact that -V is actually (!V + 1) (in 2's complement) that eats up the extra -1 is the correct explanation?
So,
SBC = A - V - 1 + C
SBC = A + !V + 1 - 1 + C
SBC = A + !V + CAt the time of writing I was sure I had it right too!
I’ll fix that up. Thanks :)
ADC = A + V + C
SBC = A - V - (1 - C)
SBC = A + (-1 * (V - (C - 1)))
SBC = A + (-V + (C - 1))
SBC = A + -(V + 1) + C
"The SBC instruction subtracts a value from the accumulator register, with an extra 1 subtracted from the result if the carry flag is NOT set."The explanation fits though. If the carry flag is not set (C == 0) then you subtract an extra 1. If C == 1 then it's A-V.
The APU sends all of its samples to a staging buffer, then between video frames, the Mutex for the SDL buffer is locked and the staging buffer is emptied into it. The SDL callback also locks the Mutex 60 times per second, takes evenly spaced samples from the raw data, and truncates what it consumed if it had more than it needed. Now the audio keeps perfect pace with the emulator, whether it slows to 58 FPS or goes too fast, and there's no popping and clicking. If you're curious, here are the relevant spots in the code now:
https://github.com/spieglt/nestur/blob/master/src/audio.rs https://github.com/spieglt/nestur/blob/e49921541d493b3616352...
I just beat the first Zelda dungeon last night, saving the file afterwards, and it was indeed really satisfying.
I'm curious: are the cycle-accurate reads and writes (and false reads and writes -- see https://github.com/zellyn/a2audit/issues/5) not necessary for NES emulation?
I've never emulated a NES, only an Apple II. The cycle-accurate reads and writes are only mostly unnecessary, but they can make a difference, as the discussion in that linked issue shows.
NES actually gets a bit grosser because the PPU runs (IIRC) five cycles for every three CPU cycles or something like that, and you need to handle that case. I've seen some emulators that just run it off of a 15x clock, and sparsely run cycles on the CPU and PPU (but most of the master cycles don't do any work).
I just called a "tick" function at the end of each cycle, which is definitely slower: https://github.com/zellyn/go6502/blob/master/cpu/opcodeinstr...
I didn't get to the point of caring about interrupt correctness, but I did port the gate-level 6502 emulation to Go, and run Klauss Dormann's fairly comprehensive test suite on both in parallel, checking that all memory reads and writes matched.
> They rely on the dummy read for the sta $4000,X instruction to acknowledge pending APU IRQs.
https://wiki.nesdev.com/w/index.php/Tricky-to-emulate_games
MMC1 ignores consecutive writes, and one games relies on the initially written false value:
> Bill & Ted's Excellent Adventure resets the MMC1 by doing INC on a ROM location containing $FF; the MMC1 sees the $FF written back and ignores the $00 written on the next cycle.
Note that my NES emulator doesn't even get the total number of cycles right, and as a result Super Mario Bros. 3 crashes. (It really isn't a good emulator!)
Then you start working on those last 10%s, which includes cycle accurate emulation. As it happens, there's a lot of work with goes into the final 10% of the various NES subsystems. Far, far more work than the first 90%.
As for what percentage of games need cycle accurate 6502 emulation I couldn't tell you.
Thanks for letting me know!