Just finished the cpu, and ROM loading
The NES (Nintendo Entertainment System) is a fantastic console able to run awsome games such as the original super mario brothers, tetris, pacman and even super mario bros 3!
Rust is relatively low-level language, focused on memory safety and developer clarity, without sacrificing high-performance. This makes it a good language to emulate the modified 6502 chip which is the core of the nes without worrying too much about optimization even in relatively "bad" computers.
This project has two main purposes: to help us learn rust, and to let us code with the homie.
This repo also tries to help us maintain "good" habits. So hopefully everything will be tested (at least in the CPU, which have a lot of tricky pars), and pre-push hooks will make sure our code is formatted and passes all the tests before we push it (it also make sure we don't have any uncommited changes).
We follow (not strictly) the guide in here. Some other useful links are:
- the rust guidebook
- a guide with details about 6502 commands
- another guide with even more details about 6502 commands
- another guide with more human friendly explanation, but with less details
- someones repo about nes emulator in c
- another new emulator repo, in python
- interactive 6502 tutorial, with the added benefit of being able to see what results some opcodes should give in given scenarios
- Sources about the graphical process:
After we implemented the cpu, we created custom screen to implement the snake game found here. This is a different binary named "SNAKE" (more about it in the how to run section). Before you run the snake game, make sure you installed sdl2 per these instructions. You may need to include the dll near where you have your exe, depending on your os.
You play the snake game with the wasd keys. The "P" key is used to pause the game at any point. The game is also
auto-paused after death (and can be released by pressing the "P" key again, or by waiting 10 seconds).

After that, we just needed to implement the ppu (Picture Processing Unit), which is the part that is responsible for actually drawing the picture. On one leg, this part has its own memory, and the cpu can communicate with the ppu memory via special registers, that are mapped to certain parts of the memory. Once the ppu memory is full, it draws the screen from this memory. This sounds easy, and shouldn't take much time, right?
... So we had to refactor major parts of our code, to allow both the cpu and the ppu to read from the same bus (due to a
design choice we made, that said the ppu and the cpu should both have a reference to the bus, instead of the more
standard approach of having the ppu reference in the bus). This, combined with the quirks of how the nes is rendering
its screen, made us change the memory read function to be change the memory (get mut &self instead of &self), which
in part caused a very weird bug that caused the background tiles to "skip" about every tile (the fix was either in
commit
95bb781fd0083c01fede4296d84dd3d910167aff or in commit ec2db0cfacc68b887e072c551ecc71ac35b47822). Rust tried to warn
us about this change in mutability, but we ignored and
paid the price.
So without further ado, let's see some images and gifs from the developing process!
We started by trying to emulate packman. We started by drawing the background tiles, with pre-determined palette.
Next, we got this wierd image instead of the home screen. That was, until we noticed the blinks were just in the place
the home screen blinked (when showing all the characters, and afterward when the big orbs are in the game). So we
realized we used the wrong bank for background sprites, and quickly fixed it.
![]()
And now for a quick build montage:
The pacman loading screen with all the extra spaces (before we fixed the memory reading). The color palette is also wrong
We fixed the spaces, and gradually fixed the colors. In the first picture we still used a pre-determined palette. Then we read from the game palette, but mixed the nametable high byte with the nametable low byte. Surely this won't happen again with the sprites, right?
These three images show the game without sprites
After we started to tackle the sprites, we got this funny video. However, we quickly fixed it
And now we added character sprites!
Now we can play mario, without the scrolling part! In the future, we implemented the screen scrolling.
Unittests are written in rust and can be run using
cargo testWe also have tests on the full CPU based on a known test suite for nes
named nestest. The test contains a ROM (can be found in
full_tests/nestes.nes), and the results of the ROM (full_tests/nestes_result_good.log). Our tests have two parts -
first we emulate the Running of the nestes.nes using our cpu, and write the result after each opcode to .txt file in
full_tests (this is in .gitignore). Then, we compare our result to the good result using a python script in the same
directory.
We check only upto line 5004, which is pc 0xc6bd, since there the opcode is 0x04, which is undocumented opcode (read about it!).
To generate our logs run
cargo run --package nes_emulator --bin gen_cpu_tests_logs -- ./full_tests/nestest.nes ./full_tests/foo.txt 0xc6bdTo run the python script, that both generate the logs (using a subprocess of the previous command) and compare them ( using itself), run
python3 .\full_tests\compare_logs.pyYou may need to install pydantic before running the script (pip install pydantic).
Using cargo you can do
cargo run --package nes_emulator --bin nes_mainOr in release mode
cargo run --package nes_emulator --bin nes_main --releaseEdit the nes_main/main.rs file.
cargo run --bin SNAKEThere is even (kind of) cli! you can choose to load the snake game from a dump (.nes file found in snake_game directory) or "hard coded" (the values are hard coded in the code). You can also choose to see some kind of basic debug print (that prints the current pc, opcode, and two bytes after the opcode). Run it with
cargo run --bin SNAKE hard_coded/dump true/falseThe values also have default values - dump and false (no debug print), but you must set the hard_coded/dump argument before passing the debug argument.





