Repository: BotRandomness/NET-NES
Branch: master
Commit: df38e9bf76a8
Files: 23
Total size: 109.5 KB
Directory structure:
gitextract_r7is6r8c/
├── .gitignore
├── LICENSE
├── NET-NES.csproj
├── README.md
├── src/
│ ├── Bus.cs
│ ├── CPU.cs
│ ├── Cartridge.cs
│ ├── GUI.cs
│ ├── Helper.cs
│ ├── Input.cs
│ ├── NES.cs
│ ├── PPU.cs
│ ├── Program.cs
│ ├── Test.cs
│ ├── TestBus.cs
│ ├── TestRunner.cs
│ ├── interface/
│ │ ├── IBus.cs
│ │ └── IMapper.cs
│ └── mappers/
│ ├── Mapper0.cs
│ ├── Mapper1.cs
│ ├── Mapper2.cs
│ └── Mapper4.cs
└── test/
└── README.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.vscode/
obj/
bin/
bulid/
publish/
extra/
test/v1/
*.bin
*.nes
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 Bot Randomness
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: NET-NES.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>NET_NES</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- Enabling unsafe code -->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Raylib-cs" Version="6.1.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ImGui.NET" Version="1.90.9.1" />
<PackageReference Include="rlImgui-cs" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<Content Include="res\**\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>
================================================
FILE: README.md
================================================
<!-- PROJECT LOGO -->
<h1 align="center">
<br>
<a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Logo.png" alt="NET-NESLogo" width="400"></a>
<br>
<b>NET-NES</b>
<br>
<sub><sup><b>NET-NES, a NES emulator, written in C#.</b></sup></sub>
<br>
</h1>
<p align="center">
<a href="https://github.com/BotRandomness/NET-NES">
<img src="git-res/Game.png" alt="ProjectImage" width="85%" height="85%">
</a>
</p>
<p align="center">
<strong>NET-NES is a NES emulator, capable of running some of the best games for the NES.</strong> <em>To start off</em>, after making my Gameboy emulator, <a href="https://github.com/BotRandomness/CODE-DMG">CODE-DMG</a>, I wanted to create the next step up. As I thought to myself, <strong>"It only made sense to create a NES emulator!"</strong> It felt such a natural pairing making a Gameboy emulator, and now NET-NES, a NES emulator. The NES to me is a very fascinating console. The NES not only made a impact in gaming history, but also in electronics, which captivated me. Not to mention how the NES is home to some of the best and well known games! So why not take on the <strong>challenge</strong> and emulate it, it would be perfect! Now, <em>some quick history and background information.</em> The NES, <em>(Nintendo Entertainment System)</em>, launched first in Japan as the <strong>FamiCom</strong> <em>(Family Computer)</em> in <strong>1983</strong>, later coming into <strong>North America in 1985</strong>. At this point in North America, the <em>Video Game Crash of 1983</em> was hitting North America hard, with home console gaming market being in shambles. However, <strong>Nintendo</strong> in 1985 decided to launch their console in North America, change the name to NES, advertise it as a "toy", and <strong>boom!</strong> That was able to <strong>bring life back</strong> into the gaming industry in North America! Pretty cool history if I say so myself. Now into the hardware itself, the NES houses a <strong>8-bit CPU</strong>, a <strong>Ricoh 2A03</strong>, which runs at around <strong>1.79 Mhz</strong>. This CPU is pretty much a clone of the <strong>MOS 6502</strong>, but with the <strong>decimal mode removed, and a APU added</strong>. The <strong>PPU</strong>, the <strong>Ricoh 2C02</strong>, runs around <strong>3 times faster then the CPU</strong>, and is reponsible for display graphics on a frame of <strong>256x240</strong> with palletes being able to pick from <strong>64 colours</strong>. When it comes to <strong>memory</strong>, there is <strong>2 KB of RAM</strong>, and <strong>2 KB of VRAM</strong> with a <strong>16-bit address bus</strong>. NES cartridges can also contain a <strong>mapper chip</strong> to support larger ROMs, larger graphics data, and more RAM. Coming back to this project, this was really fun to work on! Now you may have a few questions like, <em>"Ok, so why the name NET-NES?"</em> Well I made it using C# and Dotnet, so I thought it's <strong>pretty unique and fun to say!</strong> Now you may ask, <em>"Cool, but why C# and Raylib again? Why not C/C++, or even Java?"</em> <strong>C# has been was wonderful to use</strong> and something I keep coming back to, and the <strong>community is great</strong>. As for Raylib, <strong>I just keep finding it fun! :)</strong>
</p>
</div>
<!-- ABOUT THE PROJECT -->
## Getting Started
Want to use it, and mess around? Here's how you can get started! </br>
### Download
1. Download it from [here](https://github.com/BotRandomness/NET-NES/releases), or on the releases page. (win-x64, win-x86, osx-x64, osx-arm64, linux-x64)
2. Unzip the folder
3. Launch the executable
- On MacOS, there might be a pop up saying "Apple could not verify", this is normal. Simply right click on the app, then open, then open again. You also go to System Settings, then security, then allow. You only need to do this once during the first time.
- You may also need to enable execute permission on "Unix-like Oses"
- If you want to use the <strong>terminal</strong>: Point your terminal to the application and run, Windows: `NET-NES --nes <string:rom>`, Unix-like Oses `./NET-NES --nes <string:rom>`
4. You are ready to go!
### Controls
- (A) = X
- (B) = Z
- [START] = [ENTER]
- [SELECT] = [RSHIFT]
- D-Pad = ArrowKeys
<em>Note: Pressing [SPACE] Bar toggles the top menu bar.</em>
### Usage
Simply launching the executable will show the basic GUI. Going `File -> Open ROM` will bring up a file launcher to select your ROM. `Help -> Manual` will show all the basic instructions on how to use, and what to know. More information in the Compatibility section.
#### Flags
NET-NES flags when using terminal. Note: these flags can be passed in any order, and in any combination.
- `--nes <string:path>`: Starts up the emulator given a rom file
- `--json <string>`: Runs a CPU test for a instruction given a JSON file in test/v1
- `-s <int>`, `--scale <int>`: Scale window size by factor (2 is default)
- `-f`, `--fps`: Enables FPS counter (off is default)
- `-rl`, `--raylib-log`: Enables Raylib logs (off is default)
- `-d`, `--debug`: Enables debug mode (off is default)
- `-a`, `--about`: Shows about
- `-v`, `--version`: Shows version number
- `-h`, `--help`: Shows help screen
## Screenshots
<a href="https://github.com/BotRandomness/NET-NES">
<img src="git-res/List.png" alt="GameShowcase" width="4120%" height="700%">
</a>
<p align="center"> Showcase of running games </p>
### Demo Gameplay
<table>
<tr>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/DD.gif" alt="DonkeyKong" width="340%" height="342%"></a></td>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/SMB.gif" alt="SMB" width="340%" height="342%"></a></td>
</tr>
<tr>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Zelda.gif" alt="Zelda" width="340%" height="342%"></a></td>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Met.gif" alt="Metroid" width="340%" height="342%"></a></td>
</tr>
<tr>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Kriby.gif" alt="Kriby" width="340%" height="342%"></a></td>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Duck.gif" alt="DuckTale" width="340%" height="342%"></a></td>
</tr>
<tr>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/Contra.gif" alt="Boot" width="340%" height="342%"></a></td>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/MM.gif" alt="Tetris" width="340%" height="342%"></a></td>
</tr>
<tr>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/SMB3.gif" alt="SMB3" width="340%" height="342%"></a></td>
<td><a href="https://github.com/BotRandomness/NET-NES"><img src="git-res/NG2.gif" alt="Tetris" width="340%" height="342%"></a></td>
</tr>
</table>
## Compatibility
<p align="center">
<a href="https://github.com/BotRandomness/NET-NES">
<img src="git-res/NesTest.png" alt="nestest" width="50%" height="50%">
</a>
</p>
<p align="center">Passes nestest CPU instruction tests</p>
Most NES cartridges came with a mapper chip to support larger size of ROM and graphics data, as well as bulit in RAM on the cartridges. Mapper 0 (NROM) is most simple, and should work. Mapper 1, Mapper 2, and Mapper 4 all have been implemented and they work, but they should be consider experimental.</em>
</br>
Most games I tried out works. Some games may have some graphical glitches, or might just freeze. This is because even though I have emulated the base NES system, the emulation itself is not fully accurate. This because the PPU emulation I did runs by scanline timing, and not by dot by dot, meaning there is some timing inaccuracy that can effect a few games. More infomation in the Program Architecture section. However, most games should run fine, here is list of games I tested:
```
Balloon Fight - Works
Bubble Bobble - Works
Castlevania - Works
Castlevania II - Works
Contra - Works
Donkey Kong - Works
Dr. Mario - Works
DuckTales - Works
Execitebike - Works
Final Fantasy - Works
Final Fantasy II - Freeze at first Battle (Due to inaccurate Sprite0 Hit timming)
Final Fantasy III - Works
Galaga - Works
Ice Climber - Works
Kirby's Adventure - Works
The Legend of Zelda - Works
Mario Bros. - Works
Mega Man - Works
Mega Man 2 - Works
Mega Man 6 - Works
Metroid - Works
Ninja Gaiden - Freeze at Act 1 Screen (Due to inaccurate Sprite0 Hit timming), However you can get it to work if in debug mode you disable "Sprite0 Hit Check"
Ninja Gaiden II - Works
Ninja Gaiden III - Works
Pac-Man - Works
Super Mario Bros. - Works
Super Mario Bros. 2 - Works
Super Mario Bros. 3 - Works
Teenage Mutant Ninja Turtles Tournament Fighters - Works
Tetris - Works
Tetris 2 - Works
```
## Compile
Want to tinker around, modify, make your own, learn a bit about emulation development, or contribute? Here's how you can get started with the code and compile.
To get started, you need to have dontnet install. For reference, I used dotnet 6.0
1. Download dotnet: https://dotnet.microsoft.com/en-us/
2. Clone this repository, and point your terminal to the root directory of the repository
3. Run `dotnet run` to compile, and it should run right after! You also do `dotnet run -- --nes <string:rom>` if want to load up a rom at startup!
Raylib-cs (the C# binding (made by Chris Dill) for Raylib), does not need to be installed, as dotnet will automatically install any dependences from NuGet. For more information on raylib-cs can be found here on Github: https://github.com/chrisdill/raylib-cs. This also applies to [Rlimgui-cs](https://github.com/raylib-extras/rlImGui-cs) and [ImGui.NET](https://github.com/ImGuiNET/ImGui.NET).
### Build
- For your own platform, framework dependent: `dotnet publish`
- For other platform, single file, not framework dependent:</br> `dotnet publish -r <RID> --self-contained -o bulid/<RID-Name> /p:PublishSingleFile=true`
- For other platform, single file, framework dependent:</br> `dotnet publish -r <RID> --no-self-contained -o bulid/<RID-Name> /p:PublishSingleFile=true`
</br>
The reason to have both dotnet dependent or not is the file size. If the user already has dotnet, the lighter file size is the best option. If the user does not have dotnet, it's more convenient to bundle in the dotnet as self contained even if the file size is larger. It's best to put PublishSingleFile for convenience, especially for self contained dotnet as that will have 224 dll files all in the root of the executable.
</br></br>
For more see the dotnet publish documentation: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish, RID: https://learn.microsoft.com/en-us/dotnet/core/rid-catalog, SingleFile: https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/design.md </br> </br>
For <strong>MacOS</strong>, building requires signing, <em>especially for Apple Silicon</em>. This part is going to be a general note. Dotnet and C# when running and building will automatically sign the binaries with a simple "ad-hoc" (meaning needed or for this) signature. This needs to be done for using and distributing. On other platforms, compiling for Macs would not be signed, since Apple's `codesign` tool is only on Macs <em>(though others have made open source versions of the tool for cross-platform use)</em>. If you have a unsigned binary compiled on a non-Mac platform now on a Mac platform, a simple "ad-hoc" signing will do and can done by `codesign -s - <BinaryPath>`. When it comes `.app` bundles, signing is also required in the same way. You can bulid the `.app` bundle manually since it's just a directory (Mac build of NET-NES for reference). However, even if the `.app` bundle is made up with already signed binary, you will have to re-sign the <strong>whole</strong> `.app` bundle. This can be done with `codesign --force --deep -s - <AppPath.app>`.
### Program Architecture
Here's a little information on the program layout!
It can seem a bit complex at first, but it's quite simple. I like to write my code, where it should be easy enough for anyone to understand, no matter their knowledge with emulation, or code! With that in mind, this portion itself is written to be simple, no matter your skill level, so anybody should get the idea of the program works, it's sort of the reason why I write these parts! :)
C# object oriented design allows us to think all the componets of the NES hardware, and make them into classes, which can be used to represent a object. The root of the `src` contains all the main code. With that, we have the following:
- `CPU.cs`: The 8-Bit NES CPU (Ricoh 2A03)
- `PPU.cs`: The PPU (Picture Processing Unit) of the NES is responsible for drawing the graphics on screen
- `Bus.cs`: Contains the `Read()` and `Write()` functions, directing bus for RAM, VRAM, and the cartridge, this also there we can "wire up" some our componets here that rely on memory access
- `Cartridge.cs`: Contains memory for PRG and CHR, set's up the cartidge state depending on the iNES header
- `src/mapper`: Contains the different memory bank controllers needed, all to make the NES support larger ROMs
- `Mapper0.cs`: NROM
- `Mapper1.cs`: MMC1
- `Mapper2.cs`: UxROM
- `Mapper4.cs`: MMC3
- `Input.cs`: Handles input for the NES
- `NES.cs`: Where we "wire up" all the componets
- `Test.cs`: Used for JSON test for the CPU
- `TestBus.cs`: A simplified bus for simple memory access for testing
- `TestRunner.cs`: Runs the JSON test from `Test.cs`
- `GUI.cs`: A simple GUI to wrap the NES core
- `Helper.cs`: Static class to hold global values
- `Program.cs`: The entry point of the emulator
<em>This emulator is <strong>not meant to be cycle accurate</strong>, as the PPU does not run dot by dot, but by scanline.</em>
Each component should stick to it's task. The core of this emulator is written in plain C#, with the only outside library being raylib-cs which is used very minimally. It would be something I may do where I refactor the code to make the core more independent, which should be easy enough to do.
To see how the main loop works, we can look into the `NES.cs` in the `Run()` function:
```cs
public void Run() {
int cycles = 0;
bus.input.UpdateController();
while (cycles < 29828) {
int used = bus.cpu.ExecuteInstruction();
cycles += used;
bus.ppu.Step(used * 3);
}
bus.ppu.DrawFrame(Helper.scale);
}
```
The loop is quite simple, and uses a simple approach of having the "CPU drive". This is done my the CPU's `ExecuteInstruction()` function, where after every instruction, it return it's cycle count. We can pass this count into the PPU's `Step()` function as it's important to keep the CPU and PPU in sync. We do times by three here because the PPU runs 3x faster then the CPU. The PPU's `Step()` function using the cycle count keeps tracks of it's own cycles to perform the correct behaviour at certain cycle threshold. We have this loop run for 29828 cycles, since having 29828 cycles done represent a frame worth of cylces for around 60 FPS (NTSC).
All the other components should follows their own sturucture to do it's job. For example the `CPU` will execute a instruction using the `Bus`, reading the opcode to see what instruction to do, then writing to memory if needed or handle a interrupt. The `PPU` will take in the number of cycles for tracking, and perform certain behaviour such as after cycle relative to scanline, `scanlineCycle >= 341`, it can render a scanline. The `Bus` is what connects the `CPU` and `PPU` to memory, and it's all three components can "talk" to each other using MMIO which are get special places in memory where both componets can read and write depending on use, for example like PPUDATA `$2000` which where VRAM data can we read and write to. Let's look at `CPU.cs` as a example. A constructor is made to set the default state of the componet:
```cs
public CPU(IBus bus) {
A = X = Y = 0;
PC = 0x0000;
SP = 0x0000;
status = 0;
this.bus = bus;
irqRequested = false;
nmiRequested = false;
Console.WriteLine("CPU init");
}
```
Notice how other component can rely on others, and how they need to be passed in. In this case of the CPU, the Bus is passed in, as many insturction need to access to the memory. Each component then had the "main" method they would be invoking. For PPU called `Step()` as we step every cycle passed in. For the CPU, we have `ExecuteInstruction()`:
```cs
private byte Fetch() {
return bus.Read(PC++);
}
public int ExecuteInstruction() {
...
byte opcode = Fetch();
switch (opcode) {
//LDA, LDX, LDY, STA, STX, STY
case 0xA9: return LDR(ref A, Immediate, 2);
case 0xA5: return LDR(ref A, ZeroPage, 3);
case 0xB5: return LDR(ref A, ZeroPageX, 4);
...
private int LDR(ref byte r, Func<AddrResult> mode, int baseCycles) {
var addr = mode();
r = bus.Read(addr.address);
SetZN(r);
return baseCycles + addr.extraCycles;
}
```
If you have seen my Gameboy emulator, CODE-DMG, all of this may seem familiar. If you go over each component step by step, it's pretty each to follow along.
## Credits/Resources
NET-NES wouldn't be possible without these resources:
- NESDEV Wiki: https://www.nesdev.org/wiki/Nesdev_Wiki
- 6502 Opcodes: https://www.masswerk.at/6502/6502_instruction_set.html
- Obelisk 6502 Documentation: https://www.nesdev.org/obelisk-6502-guide/
- SingleStepTest JSON Test 65x02: https://github.com/SingleStepTests/65x02
- MOS Microcomputer Programming Manual: https://archive.org/details/mos_microcomputers_programming_manual
- MOS Microcomputer Hardware Manual: https://web.archive.org/web/20221106105459if_/http://archive.6502.org/books/mcs6500_family_hardware_manual.pdf
- Rodrigo Copetti's NES/Famicom Architecture A Practical Analysis: https://www.copetti.org/writings/consoles/nes/
These documentations were so useful, I recommend anyone to use them!
Also shoutout to the EmuDev community!
## Upcoming Features
- [ ] Debugger
- VRAM viewer, Memory viewer, step mode
- [ ] Add More Mappers
- Mapper 3
- [ ] Player 2 Controller Support
- [ ] Audio
- Post any feature request in the Issues tab!
## Known issues
- [ ] Upgrade PPU to dot by dot
- Will make it more accurate
- [ ] Improve interrupt logic
- If you find other bugs/issues, open up a issue in the Issue tab
## Remarks
What a adventure it was to make Gameboy emulator and a NES emulator back to back. I find hard to believe that a year ago I just finished making my first emulator for the CHIP-8 not knowing anything about low-level hardware, and now after making four emulators <em>(CHIP-8, Intel 8080, Gameboy, NES)</em>, it feels great. As with anything I make, I not only like to share the software itself, but also like the aspect of how other furture NES emulator developers can reference if they get stuck on something, or if anyone is just interested on how emulation works. Emulation is one of those things that seem like magic, even for a experienced developer. That's why I always have the goal to write code as simple and clear as possible, and hope it can help someone else in the future :)
If anyone reading is curious, here is how I went about making my NES emulator. So before I actually talk about that, I want to give my two cent on a common question asked in emulation development. *Should one make a Gameboy emulator first or a NES emulator first? Which one is "easier"?* To that, I say it really depends on your goals. Making a Gameboy emulator can take a bit longer to have fully playable games, but once you get a simple game like Tetris, most games starts to work. The NES, you can get games like Donkey Kong playable in early development of your emulator, but to emulate it *perfectly* to play all games is much more work. This because for a Gameboy emulator, you can make a scanline based render without cycle accuracy, and most games would work perfectly. For the NES, that's really not the case. You can get a lot of games working with scanline timing, but having a dot by dot is pretty important if you want most. Another thing to note, I personally found more documenation and information for the Gameboy to be eaier to follow than the NES. When it comes to difficulty of implementation, in hindsight, they both felt about the same. I found it good that I made a Gameboy emulator first, and so then using that knowledge to make a NES emulator was useful. With that in mind, my first goal when it came to making a NES emulator is making sure I had a completed 6502 CPU that were passing the JSON test. Making the CPU was pretty easy since it had less instructions then the Gameboy's CPU. The only thing I had to look out for was the addressing modes. I made a small bug early on where I had a extra cycle, which when it came to doing basic PPU rendering, caused graphical glitches. After realizing that, a quick fix got everything back on track. Now when I started on the PPU, instead of jumping right into scanline rendering, I made a simple full frame renderer. I did this because I already knew the NES's PPU can be a bit tricky to implement in full. Games like Donkey Kong will work perfectly fine with a simple full frame rendering, with only the basic behaviour of the PPU. Then after confirming my simple renderer works with basic inputs, I upgraded it a scaline based one, then added scrolling using Super Mario Bros as a test for that. At this point Mapper 0 games worked fine, so I decided to support the other mappers. I hope reading this little simple remark and looking though this repository was useful to someone on their own emulation journey or someone who is just interested. Of course, the best resource for making a NES emulator is the [NesDev](https://www.nesdev.org/wiki/Nesdev_Wiki) Wiki, it's so helpful. Thank you for reading!
```
____________________________
│ │ NES │---│ │
│ │____________________│___│ │
│____________________________│
| 1 2 |
\ ■ [ ] [ ] ▒ ▒ /
∙------------------------∙
BotRandomness
^ ASCII NES art I made myself
Free to use. If used, credit is not needed, but is appreciated :)
```
## Contributing
This project is open-source under the MIT License, meaning your free to do what ever you want with it. This project is freely available for anyone to contribute, emulations experts, Nintendo fans, NES lovers, retro enthusiast, or someone who is new to it all.
If you plan on contributing, a good place to start is to look at upcoming wanted features, and known issues. If you find a new bug, or have feature ideas of your own, posted first to the Issues tab before hand. You can even fork it and make it your own! </br>
To get started on contributing:
1. Fork or Clone the Project
2. Once you have your own repository (it can be a public repository) to work in, you can get started on what you want to do!
3. Make sure you git Add and git Commit your Changes to your repository
4. Then git push to your repository
5. Open a Pull Request in this repository, where your changes will be look at to be approved
6. Once it's approved, it will be in a development branch, soon to be merge to the main branch
<!-- LICENSE -->
## License
Distributed under the MIT License. See `LICENSE` for more information.
================================================
FILE: src/Bus.cs
================================================
public class Bus : IBus{
public CPU cpu;
public PPU ppu;
public Cartridge cartridge;
public byte[] ram; //2KB RAM
public Input input = new Input();
public Bus(Cartridge cartridge) {
this.cartridge = cartridge;
cpu = new CPU(this);
ppu = new PPU(this);
ram = new byte[2048];
Console.WriteLine("Bus init");
}
public byte Read(ushort address) {
if (address == 0x4016) {
return input.Read4016(); //NES controller input
}
if (address >= 0x2000 && address <= 0x3FFF) {
ushort reg = (ushort)(0x2000 + (address & 0x0007));
byte result = ppu.ReadPPURegister(reg);
return result;
} else if (address >= 0x0000 && address < 0x2000) {
return ram[address & 0x07FF];
} else if (address >= 0x6000 && address <= 0xFFFF) {
return cartridge.CPURead(address);
}
return 0;
}
public void Write(ushort address, byte value) {
if (address == 0x4016) {
input.Write4016(value);
return;
}
if (address == 0x4014) {
ppu.WriteOAMDMA(value);
return;
}
if (address >= 0x2000 && address <= 0x3FFF) {
ushort reg = (ushort)(0x2000 + (address & 0x0007));
ppu.WritePPURegister(reg, value);
} else if (address >= 0x0000 && address < 0x2000) {
ram[address & 0x07FF] = value;
} else if (address >= 0x6000 && address <= 0xFFFF) {
cartridge.CPUWrite(address, value);
}
}
}
================================================
FILE: src/CPU.cs
================================================
public class CPU {
public byte A, X, Y;
public ushort PC, SP;
public byte status; //Flags (P)
private const int FLAG_C = 0; //Carry
private const int FLAG_Z = 1; //Zero
private const int FLAG_I = 2; //Interrupt
private const int FLAG_D = 3; //Decimal Mode (Unused in NES)
private const int FLAG_B = 4; //Break Command
private const int FLAG_UNUSED = 5; //Used bit 5 (always set)
private const int FLAG_V = 6; //Overflow
private const int FLAG_N = 7; //Negative
private IBus bus;
private bool irqRequested;
private bool nmiRequested;
public CPU(IBus bus) {
A = X = Y = 0;
PC = 0x0000;
SP = 0x0000;
status = 0;
this.bus = bus;
irqRequested = false;
nmiRequested = false;
//Reset();
Console.WriteLine("CPU init");
}
public void Reset() {
A = X = Y = 0;
SP = 0xFD;
status = 0x24;
byte low = bus.Read(0xFFFC);
byte high = bus.Read(0xFFFD);
PC = (ushort)((high << 8) | low);
}
public void SetFlag(int bit, bool value) {
if (value) {
status |= (byte)(1 << bit);
} else {
status &= (byte)~(1 << bit);
}
}
public bool GetFlag(int bit) {
return (status & (1 << bit)) != 0;
}
public void SetZN(byte value) {
SetFlag(FLAG_Z, value == 0); //Zero
SetFlag(FLAG_N, (value & 0x80) != 0); //Negative
}
/*
public void Log() {
ushort op1 = (PC);
ushort op2 = (ushort)(PC + 1);
ushort op3 = (ushort)(PC + 2);
ushort op4 = (ushort)(PC + 3);
//Console.WriteLine("A: " + A.ToString("X2") + " X: " + X.ToString("X2") + " Y: " + Y.ToString("X2") + " SP: " + SP.ToString("X4") + " PC: " + "00:" + PC.ToString("X4") + " (" + mmu.Read(op1).ToString("X2") + " " + mmu.Read(op2).ToString("X2") + " " + mmu.Read(op3).ToString("X2") + " " + mmu.Read(op4).ToString("X2") + ")");
byte b1 = bus.Read(op1);
byte b2 = bus.Read(op2);
byte b3 = bus.Read(op3);
byte b4 = bus.Read(op4);
Console.WriteLine("A: " + A.ToString("X2") + " X: " + X.ToString("X2") +" Y: " + Y.ToString("X2") + "P: "+ Convert.ToString(status, 2).PadLeft(8, '0') + " SP: " + SP.ToString("X4") + " PC: :" + PC.ToString("X4") + " (" + b1.ToString("X2") + " " + b2.ToString("X2") + " " + b3.ToString("X2") + " " + b4.ToString("X2") + ")");
}
*/
private byte Fetch() {
return bus.Read(PC++);
}
public ushort Fetch16Bits() {
byte low = Fetch();
byte high = Fetch();
return (ushort)((high << 8) | low);
}
public void RequestIRQ(bool line) {
irqRequested = line;
}
public void RequestNMI() {
nmiRequested = true;
}
public int ExecuteInstruction() {
if (nmiRequested) {
nmiRequested = false;
return NMI();
}
if (GetFlag(FLAG_I) == false && irqRequested) {
irqRequested = false;
return IRQ();
}
byte opcode = Fetch();
switch (opcode) {
//BRK, NOP, RTI
case 0x00: return BRK();
case 0xEA: return NOP();
case 0x40: return RTI();
//LDA, LDX, LDY, STA, STX, STY
case 0xA9: return LDR(ref A, Immediate, 2);
case 0xA5: return LDR(ref A, ZeroPage, 3);
case 0xB5: return LDR(ref A, ZeroPageX, 4);
case 0xAD: return LDR(ref A, Absolute, 4);
case 0xBD: return LDR(ref A, AbsoluteX, 4);
case 0xB9: return LDR(ref A, AbsoluteY, 4);
case 0xA1: return LDR(ref A, IndirectX, 6);
case 0xB1: return LDR(ref A, IndirectY, 5);
case 0xA2: return LDR(ref X, Immediate, 2);
case 0xA6: return LDR(ref X, ZeroPage, 3);
case 0xB6: return LDR(ref X, ZeroPageY, 4);
case 0xAE: return LDR(ref X, Absolute, 4);
case 0xBE: return LDR(ref X, AbsoluteY, 4);
case 0xA0: return LDR(ref Y, Immediate, 2);
case 0xA4: return LDR(ref Y, ZeroPage, 3);
case 0xB4: return LDR(ref Y, ZeroPageX, 4);
case 0xAC: return LDR(ref Y, Absolute, 4);
case 0xBC: return LDR(ref Y, AbsoluteX, 4);
case 0x85: return STR(ref A, ZeroPage, 3);
case 0x95: return STR(ref A, ZeroPageX, 4);
case 0x8D: return STR(ref A, Absolute, 4);
case 0x9D: return STR(ref A, AbsoluteX, 5);
case 0x99: return STR(ref A, AbsoluteY, 5);
case 0x81: return STR(ref A, IndirectX, 6);
case 0x91: return STR(ref A, IndirectY, 6);
case 0x86: return STR(ref X, ZeroPage, 3);
case 0x96: return STR(ref X, ZeroPageY, 4);
case 0x8E: return STR(ref X, Absolute, 4);
case 0x84: return STR(ref Y, ZeroPage, 3);
case 0x94: return STR(ref Y, ZeroPageX, 4);
case 0x8C: return STR(ref Y, Absolute, 4);
//TAX, TAY, TXA, TYA
case 0xAA: return TRR(ref X, ref A, Implied, 2);
case 0xA8: return TRR(ref Y, ref A, Implied, 2);
case 0x8A: return TRR(ref A, ref X, Implied, 2);
case 0x98: return TRR(ref A, ref Y, Implied, 2);
//TSX, TXS, PHA, PHP, PLA, PLP
case 0xBA: return TSX(Implied, 2);
case 0x9A: return TXS(Implied, 2);
case 0x48: return PHA(Implied, 3);
case 0x08: return PHP(Implied, 3);
case 0x68: return PLA(Implied, 4);
case 0x28: return PLP(Implied, 4);
//AND, EOR, ORA, BIT
case 0x29: return AND(Immediate, 2);
case 0x25: return AND(ZeroPage, 3);
case 0x35: return AND(ZeroPageX, 4);
case 0x2D: return AND(Absolute, 4);
case 0x3D: return AND(AbsoluteX, 4);
case 0x39: return AND(AbsoluteY, 4);
case 0x21: return AND(IndirectX, 6);
case 0x31: return AND(IndirectY, 5);
case 0x49: return EOR(Immediate, 2);
case 0x45: return EOR(ZeroPage, 3);
case 0x55: return EOR(ZeroPageX, 4);
case 0x4D: return EOR(Absolute, 4);
case 0x5D: return EOR(AbsoluteX, 4);
case 0x59: return EOR(AbsoluteY, 4);
case 0x41: return EOR(IndirectX, 6);
case 0x51: return EOR(IndirectY, 5);
case 0x09: return ORA(Immediate, 2);
case 0x05: return ORA(ZeroPage, 3);
case 0x15: return ORA(ZeroPageX, 4);
case 0x0D: return ORA(Absolute, 4);
case 0x1D: return ORA(AbsoluteX, 4);
case 0x19: return ORA(AbsoluteY, 4);
case 0x01: return ORA(IndirectX, 6);
case 0x11: return ORA(IndirectY, 5);
case 0x24: return BIT(ZeroPage, 3);
case 0x2C: return BIT(Absolute, 4);
//ADC, SBC, CMP, CPX, CPY
case 0x69: return ADC(Immediate, 2);
case 0x65: return ADC(ZeroPage, 3);
case 0x75: return ADC(ZeroPageX, 4);
case 0x6D: return ADC(Absolute, 4);
case 0x7D: return ADC(AbsoluteX, 4);
case 0x79: return ADC(AbsoluteY, 4);
case 0x61: return ADC(IndirectX, 6);
case 0x71: return ADC(IndirectY, 5);
case 0xE9: return SBC(Immediate, 2);
case 0xE5: return SBC(ZeroPage, 3);
case 0xF5: return SBC(ZeroPageX, 4);
case 0xED: return SBC(Absolute, 4);
case 0xFD: return SBC(AbsoluteX, 4);
case 0xF9: return SBC(AbsoluteY, 4);
case 0xE1: return SBC(IndirectX, 6);
case 0xF1: return SBC(IndirectY, 5);
case 0xC9: return CPR(A, Immediate, 2);
case 0xC5: return CPR(A, ZeroPage, 3);
case 0xD5: return CPR(A, ZeroPageX, 4);
case 0xCD: return CPR(A, Absolute, 4);
case 0xDD: return CPR(A, AbsoluteX, 4);
case 0xD9: return CPR(A, AbsoluteY, 4);
case 0xC1: return CPR(A, IndirectX, 6);
case 0xD1: return CPR(A, IndirectY, 5);
case 0xE0: return CPR(X, Immediate, 2);
case 0xE4: return CPR(X, ZeroPage, 3);
case 0xEC: return CPR(X, Absolute, 4);
case 0xC0: return CPR(Y, Immediate, 2);
case 0xC4: return CPR(Y, ZeroPage, 3);
case 0xCC: return CPR(Y, Absolute, 4);
//INC, INX, INY, DEC, DEX, DEY
case 0xE6: return INC(ZeroPage, 5);
case 0xF6: return INC(ZeroPageX, 6);
case 0xEE: return INC(Absolute, 6);
case 0xFE: return INC(AbsoluteX, 7);
case 0xE8: return INR(ref X, Implied, 2);
case 0xC8: return INR(ref Y, Implied, 2);
case 0xC6: return DEC(ZeroPage, 5);
case 0xD6: return DEC(ZeroPageX, 6);
case 0xCE: return DEC(Absolute, 6);
case 0xDE: return DEC(AbsoluteX, 7);
case 0xCA: return DER(ref X, Implied, 2);
case 0x88: return DER(ref Y, Implied, 2);
//ASL, LSR, ROL, ROR
case 0x0A: return ASL(Accumulator, 2);
case 0x06: return ASL(ZeroPage, 5);
case 0x16: return ASL(ZeroPageX, 6);
case 0x0E: return ASL(Absolute, 6);
case 0x1E: return ASL(AbsoluteX, 7);
case 0x4A: return LSR(Accumulator, 2);
case 0x46: return LSR(ZeroPage, 5);
case 0x56: return LSR(ZeroPageX, 6);
case 0x4E: return LSR(Absolute, 6);
case 0x5E: return LSR(AbsoluteX, 7);
case 0x2A: return ROL(Accumulator, 2);
case 0x26: return ROL(ZeroPage, 5);
case 0x36: return ROL(ZeroPageX, 6);
case 0x2E: return ROL(Absolute, 6);
case 0x3E: return ROL(AbsoluteX, 7);
case 0x6A: return ROR(Accumulator, 2);
case 0x66: return ROR(ZeroPage, 5);
case 0x76: return ROR(ZeroPageX, 6);
case 0x6E: return ROR(Absolute, 6);
case 0x7E: return ROR(AbsoluteX, 7);
//JMP, JSR, RTS
case 0x4C: return JMP(Absolute, 3);
case 0x6C: return JMP(Indirect, 5);
case 0x20: return JSR();
case 0x60: return RTS();
//BCC, BCS, BEQ, BMI, BNE, BPL, BVC, BVS
case 0x90: return BIF(!GetFlag(FLAG_C), Relative, 2);
case 0xB0: return BIF(GetFlag(FLAG_C), Relative, 2);
case 0xF0: return BIF(GetFlag(FLAG_Z), Relative, 2);
case 0x30: return BIF(GetFlag(FLAG_N), Relative, 2);
case 0xD0: return BIF(!GetFlag(FLAG_Z), Relative, 2);
case 0x10: return BIF(!GetFlag(FLAG_N), Relative, 2);
case 0x50: return BIF(!GetFlag(FLAG_V), Relative, 2);
case 0x70: return BIF(GetFlag(FLAG_V), Relative, 2);
//CLC, CLD, CLI, CLV, SEC, SED, SEI
case 0x18: return FSC(FLAG_C, false, Implied, 2);
case 0xD8: return FSC(FLAG_D, false, Implied, 2);
case 0x58: return FSC(FLAG_I, false, Implied, 2);
case 0xB8: return FSC(FLAG_V, false, Implied, 2);
case 0x38: return FSC(FLAG_C, true, Implied, 2);
case 0xF8: return FSC(FLAG_D, true, Implied, 2);
case 0x78: return FSC(FLAG_I, true, Implied, 2);
default:
Console.WriteLine("Unimplemented Opcode: " + opcode.ToString("X2") + " , PC: " + (PC-1).ToString("X4"));
Environment.Exit(1);
return 0;
}
}
//Load/Store Operations
private int LDR(ref byte r, Func<AddrResult> mode, int baseCycles) {
var addr = mode();
//r = addr.value;
r = bus.Read(addr.address);
SetZN(r);
return baseCycles + addr.extraCycles;
}
private int STR(ref byte r, Func<AddrResult> mode, int baseCycles) {
var addr = mode();
bus.Write(addr.address, r);
return baseCycles; //No extra cycle to add
}
//Register Transfer
private int TRR(ref byte r1, ref byte r2, Func<AddrResult> mode, int baseCycles) {
r1 = r2;
SetZN(r1);
return baseCycles;
}
//Stack Operations
private void StackPush(byte value) {
bus.Write((ushort)(0x0100 + SP), value);
SP--;
SP &= 0x00FF;
}
private byte StackPop() {
SP++;
SP &= 0x00FF;
return bus.Read((ushort)(0x0100 + SP));
}
private int TSX(Func<AddrResult> mode, int baseCycles) {
X = (byte)SP;
SetZN(X);
return baseCycles;
}
private int TXS(Func<AddrResult> mode, int baseCycles) {
SP = X;
return baseCycles;
}
private int PHA(Func<AddrResult> mode, int baseCycles) {
StackPush(A);
return baseCycles;
}
private int PHP(Func<AddrResult> mode, int baseCycles) {
StackPush((byte)(status | (1 << FLAG_B) | (1 << FLAG_UNUSED)));
return baseCycles;
}
private int PLA(Func<AddrResult> mode, int baseCycles) {
A = StackPop();
SetZN(A);
return baseCycles;
}
private int PLP(Func<AddrResult> mode, int baseCycles) {
status = StackPop();
SetFlag(FLAG_UNUSED, true);
SetFlag(FLAG_B, false);
return baseCycles;
}
//Logical
private int AND(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
A = (byte)(A & bus.Read(addr.address));
SetZN(A);
return baseCycles + addr.extraCycles;
}
private int EOR(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
A = (byte)(A ^ bus.Read(addr.address));
SetZN(A);
return baseCycles + addr.extraCycles;
}
private int ORA(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
A = (byte)(A | bus.Read(addr.address));
SetZN(A);
return baseCycles + addr.extraCycles;
}
private int BIT(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte value = bus.Read(addr.address);
SetFlag(FLAG_Z, (A & value) == 0);
SetFlag(FLAG_N, (value & 0x80) != 0);
SetFlag(FLAG_V, (value & 0x40) != 0);
return baseCycles + addr.extraCycles;
}
//Arithmetic
private int ADC(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
ushort sum = (ushort)(A + bus.Read(addr.address) + (GetFlag(FLAG_C) ? 1 : 0));
SetFlag(FLAG_C, sum > 0xFF);
SetFlag(FLAG_Z, (sum & 0xFF) == 0);
SetFlag(FLAG_N, (sum & 0x80) != 0);
SetFlag(FLAG_V, (~(A ^ bus.Read(addr.address)) & (A ^ sum) & 0x80) != 0);
A = (byte)(sum & 0xFF);
return baseCycles + addr.extraCycles;
}
private int SBC(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
ushort value = (ushort)(bus.Read(addr.address) ^ 0xFF);
ushort sum = (ushort)(A + value + (GetFlag(FLAG_C) ? 1 : 0));
SetFlag(FLAG_C, sum > 0xFF);
SetFlag(FLAG_Z, (sum & 0xFF) == 0);
SetFlag(FLAG_N, (sum & 0x80) != 0);
SetFlag(FLAG_V, ((A ^ sum) & (value ^ sum) & 0x80) != 0);
A = (byte)(sum & 0xFF);
return baseCycles + addr.extraCycles;
}
private int CPR(byte r, Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte M = bus.Read(addr.address);
ushort temp = (ushort)(r - M);
SetFlag(FLAG_C, r >= M);
SetFlag(FLAG_Z, (temp & 0xFF) == 0);
SetFlag(FLAG_N, (temp & 0x80) != 0);
return baseCycles + addr.extraCycles;
}
//Increments and Decrements
private int INC(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte result = (byte)(bus.Read(addr.address) + 1);
bus.Write(addr.address, result);
SetZN(result);
return baseCycles; //No extra cycle to add
}
private int DEC(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte result = (byte)(bus.Read(addr.address) - 1);
bus.Write(addr.address, result);
SetZN(result);
return baseCycles; //No extra cycle to add
}
private int INR(ref byte r, Func<AddrResult> mode, int baseCycles) {
r++;
SetZN(r);
return baseCycles;
}
private int DER(ref byte r, Func<AddrResult> mode, int baseCycles) {
r--;
SetZN(r);
return baseCycles;
}
//Shifts
private int ASL(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte value = mode == Accumulator ? A : bus.Read(addr.address);
SetFlag(FLAG_C, (value & 0x80) != 0);
byte result = (byte)(value << 1);
if (mode == Accumulator) {
A = result;
} else {
bus.Write(addr.address, result);
}
SetZN(result);
return baseCycles;
}
private int LSR(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte value = mode == Accumulator ? A : bus.Read(addr.address);
SetFlag(FLAG_C, (value & 0x01) != 0);
byte result = (byte)(value >> 1);
if (mode == Accumulator) {
A = result;
} else {
bus.Write(addr.address, result);
}
SetZN(result);
return baseCycles;
}
private int ROL(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte value = mode == Accumulator ? A : bus.Read(addr.address);
bool oldCarry = GetFlag(FLAG_C);
SetFlag(FLAG_C, (value & 0x80) != 0);
byte result = (byte)((value << 1) | (oldCarry ? 1 : 0));
if (mode == Accumulator) {
A = result;
} else {
bus.Write(addr.address, result);
}
SetZN(result);
return baseCycles;
}
private int ROR(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
byte value = mode == Accumulator ? A : bus.Read(addr.address);
bool oldCarry = GetFlag(FLAG_C);
SetFlag(FLAG_C, (value & 0x01) != 0);
byte result = (byte)((value >> 1) | (oldCarry ? 0x80 : 0));
if (mode == Accumulator) {
A = result;
} else {
bus.Write(addr.address, result);
}
SetZN(result);
return baseCycles;
}
//Jumps and Calls
private int JMP(Func<AddrResult> mode, int baseCycles) {
var addr = mode();
PC = addr.address;
return baseCycles;
}
private int JSR() {
ushort targetLow = Fetch();
ushort targetHigh = Fetch();
ushort targetAddr = (ushort)((targetHigh << 8) | targetLow);
ushort returnAddr = (ushort)(PC - 1);
StackPush((byte)((returnAddr >> 8) & 0xFF));
StackPush((byte)(returnAddr & 0xFF));
PC = targetAddr;
return 6;
}
private int RTS() {
byte low = StackPop();
byte high = StackPop();
PC = (ushort)(((high << 8) | low) + 1);
return 6;
}
//Branches
private int BIF(bool condition, Func<AddrResult> mode, int baseCycles) {
var addr = mode();
int extra = 0;
if (condition) {
PC = addr.address;
extra = 1 + addr.extraCycles;
}
return baseCycles + extra;
}
//Status Flag Changes
private int FSC(int bit, bool state, Func<AddrResult> mode, int baseCycles) {
SetFlag(bit, state);
return baseCycles;
}
//System Functions
private int NOP() {
return 2;
}
private int BRK() {
PC++;
StackPush((byte)((PC >> 8) & 0xFF));
StackPush((byte)(PC & 0xFF));
byte pushedStatus = (byte)(status | (1 << FLAG_B) | (1 << FLAG_UNUSED));
StackPush(pushedStatus);
SetFlag(FLAG_B, false);
SetFlag(FLAG_I, true);
byte lo = bus.Read(0xFFFE);
byte hi = bus.Read(0xFFFF);
PC = (ushort)((hi << 8) | lo);
return 7;
}
private int RTI() {
status = StackPop();
SetFlag(FLAG_UNUSED, true);
SetFlag(FLAG_B, false);
byte low = StackPop();
byte high = StackPop();
PC = (ushort)((high << 8) | low);
return 6;
}
public int IRQ() {
if (GetFlag(FLAG_I) == false) {
StackPush((byte)((PC >> 8) & 0xFF));
StackPush((byte)(PC & 0xFF));
SetFlag(FLAG_B, false);
SetFlag(FLAG_UNUSED, true);
StackPush(status);
SetFlag(FLAG_I, true);
byte low = bus.Read(0xFFFE);
byte high = bus.Read(0xFFFF);
PC = (ushort)((high << 8) | low);
return 7;
}
return 0;
}
public int NMI() {
StackPush((byte)((PC >> 8) & 0xFF));
StackPush((byte)(PC & 0xFF));
SetFlag(FLAG_B, false);
SetFlag(FLAG_UNUSED, true);
StackPush(status);
SetFlag(FLAG_I, true);
byte low = bus.Read(0xFFFA);
byte high = bus.Read(0xFFFB);
PC = (ushort)((high << 8) | low);
return 7;
}
private struct AddrResult {
public ushort address;
public int extraCycles;
public AddrResult(ushort addr, int extra) {
address = addr;
extraCycles = extra;
}
}
private AddrResult Implied() {
return new AddrResult(0, 0);
}
private AddrResult Accumulator() {
return new AddrResult(0, 0);
}
private AddrResult Immediate() {
return new AddrResult(PC++, 0);
}
private AddrResult ZeroPage() {
byte addr = Fetch();
return new AddrResult(addr, 0);
}
private AddrResult ZeroPageX() {
byte baseAddr = Fetch();
byte addr = (byte)(baseAddr + X);
return new AddrResult(addr, 0);
}
private AddrResult ZeroPageY() {
byte baseAddr = Fetch();
byte addr = (byte)(baseAddr + Y);
return new AddrResult(addr, 0);
}
private AddrResult Absolute() {
ushort addr = Fetch16Bits();
return new AddrResult(addr, 0);
}
private AddrResult AbsoluteX() {
ushort baseAddr = Fetch16Bits();
ushort effective = (ushort)(baseAddr + X);
int penalty = HasPageCrossPenalty(baseAddr, effective) ? 1 : 0;
return new AddrResult(effective, penalty);
}
private AddrResult AbsoluteY() {
ushort baseAddr = Fetch16Bits();
ushort effective = (ushort)(baseAddr + Y);
int penalty = HasPageCrossPenalty(baseAddr, effective) ? 1 : 0;
return new AddrResult(effective, penalty);
}
private AddrResult IndirectX() {
byte zp = Fetch();
byte ptr = (byte)(zp + X);
ushort addr = (ushort)(bus.Read(ptr) | (bus.Read((byte)(ptr + 1)) << 8));
return new AddrResult(addr, 0);
}
private AddrResult IndirectY() {
byte zp = Fetch();
ushort baseAddr = (ushort)(bus.Read(zp) | (bus.Read((byte)(zp + 1)) << 8));
ushort effective = (ushort)(baseAddr + Y);
int penalty = HasPageCrossPenalty(baseAddr, effective) ? 1 : 0;
return new AddrResult(effective, penalty);
}
private AddrResult Indirect() {
ushort ptr = Fetch16Bits();
byte lo = bus.Read(ptr);
byte hi = (ptr & 0x00FF) == 0x00FF ? bus.Read((ushort)(ptr & 0xFF00)) : bus.Read((ushort)(ptr + 1));
ushort addr = (ushort)((hi << 8) | lo);
return new AddrResult(addr, 0);
}
private AddrResult Relative() {
sbyte offset = (sbyte)Fetch();
ushort target = (ushort)(PC + offset);
int penalty = HasPageCrossPenalty(PC, target) ? 1 : 0;
return new AddrResult(target, penalty);
}
private bool HasPageCrossPenalty(ushort baseAddr, ushort effectiveAddr) {
return (baseAddr & 0xFF00) != (effectiveAddr & 0xFF00);
}
}
================================================
FILE: src/Cartridge.cs
================================================
public class Cartridge {
public byte[] rom;
public byte[] prgROM;
public byte[] chrROM;
public int prgBanks;
public int chrBanks;
public int mapperID;
public bool mirrorHorizontal;
public bool mirrorVertical;
public Mirroring mirroringMode;
public bool hasBattery;
public byte[] prgRAM;
public byte[] chrRAM;
public IMapper mapper;
public Cartridge(string romPath) {
rom = File.ReadAllBytes(romPath);
if (rom[0] != 'N' || rom[1] != 'E' || rom[2] != 'S' || rom[3] != 0x1A) {
Console.WriteLine("Invalid iNES Header!");
Environment.Exit(1);
}
prgBanks = rom[4];
chrBanks = rom[5];
byte flag6 = rom[6];
byte flag7 = rom[7];
mirrorVertical = (flag6 & 0x01) != 0;
mirrorHorizontal = !mirrorVertical;
hasBattery = (flag6 & 0x02) != 0;
if ((flag6 & 0x08) != 0) {
} else if ((flag6 & 0x01) != 0) {
mirroringMode = Mirroring.Vertical;
} else {
mirroringMode = Mirroring.Horizontal;
}
mapperID = flag6 >> 4 | ((flag7 >> 4) << 4);
int prgSize = prgBanks * 16 * 1024;
int chrSize = chrBanks * 8 * 1024;
int offset = 16; //iNES rom is 16 bytes
prgROM = new byte[prgSize];
Array.Copy(rom, offset, prgROM, 0, prgSize);
offset += prgSize;
chrROM = new byte[chrSize];
Array.Copy(rom, offset, chrROM, 0, chrSize);
prgRAM = new byte[8 * 1024];
chrRAM = new byte[8 * 1024];
switch (mapperID) {
case 0:
mapper = new Mapper0(this);
break;
case 1:
mapper = new Mapper1(this);
break;
case 2:
mapper = new Mapper2(this);
break;
case 4:
mapper = new Mapper4(this);
break;
default:
Console.WriteLine("Mapper " + mapperID + " is not supported");
Environment.Exit(1);
break;
}
mapper.Reset();
//Console.WriteLine($"Cartridge loaded: Mapper {mapperID}, PRG {prgBanks * 16}KB, CHR {(chrSize > 0 ? chrBanks * 8 : 8)}KB");
Console.WriteLine($"Cartridge loaded: Mapper {mapperID}, PRG-ROM {prgBanks * 16}KB, {(chrSize > 0 ? $"{chrBanks * 8}KB CHR-ROM" : "CHR-RAM")}");
}
public byte CPURead(ushort address) {
return mapper.CPURead(address);
}
public void CPUWrite(ushort address, byte value) {
mapper.CPUWrite(address, value);
}
public byte PPURead(ushort address) {
return mapper.PPURead(address);
}
public void PPUWrite(ushort address, byte value) {
mapper.PPUWrite(address, value);
}
public void SetMirroring(Mirroring mode) {
mirroringMode = mode;
mirrorVertical = mode == Mirroring.Vertical;
mirrorHorizontal = mode == Mirroring.Horizontal;
}
}
public enum Mirroring {
Horizontal,
Vertical,
SingleScreenA,
SingleScreenB
}
================================================
FILE: src/GUI.cs
================================================
#pragma warning disable
using Raylib_cs;
using rlImGui_cs;
using ImGuiNET;
public class GUI {
private NES nes;
private FileDialog fileDialog;
private string selectedFilePath = "";
private bool showScaleWindow = false;
private bool showAboutWindow = false;
private bool showManualWindow = false;
Image icon;
Texture2D backgroundTexture;
public GUI() {
if (!Helper.raylibLog) Raylib.SetTraceLogLevel(TraceLogLevel.None);
Raylib.InitWindow(256 * Helper.scale, 240 * Helper.scale, "NES");
Raylib.SetTargetFPS(60);
rlImGui.Setup(true);
ImGui.GetIO().ConfigFlags |= ImGuiConfigFlags.DockingEnable;
//Unsafe access for no imgui.ini files
var io = ImGui.GetIO();
unsafe {
IntPtr ioPtr = (IntPtr)io.NativePtr;
ImGuiIO* imguiIO = (ImGuiIO*)ioPtr.ToPointer();
imguiIO->IniFilename = null;
}
fileDialog = new FileDialog(Directory.GetCurrentDirectory());
icon = Raylib.LoadImage(Path.Combine(AppContext.BaseDirectory, "res", "Logo.png"));
backgroundTexture = Raylib.LoadTexture(Path.Combine(AppContext.BaseDirectory, "res", "Background.png"));
Raylib.SetWindowIcon(icon);
}
public void Run() {
while (!Raylib.WindowShouldClose()) {
Raylib.SetWindowSize(256 * Helper.scale, 240 * Helper.scale);
Raylib.BeginDrawing();
rlImGui.Begin();
Raylib.ClearBackground(Color.Black);
MenuBar();
if (Helper.romPath.Length != 0 && Helper.insertingRom == false) {
nes.Run();
} else if (Helper.insertingRom == true) {
nes = new NES();
Helper.insertingRom = false;
} else {
Raylib.ClearBackground(Color.DarkGray);
Raylib.DrawTextureEx(backgroundTexture, new System.Numerics.Vector2(0, -5), 0, (float)(Helper.scale*0.50), Color.White);
}
if (Raylib.IsKeyPressed(KeyboardKey.Space)) Helper.showMenuBar = !Helper.showMenuBar;
if (Helper.fpsEnable) Raylib.DrawFPS(0, Helper.showMenuBar ? 19 : 0);
rlImGui.End();
Raylib.EndDrawing();
}
Raylib.CloseWindow();
}
public void MenuBar() {
if (Helper.showMenuBar) {
if (ImGui.BeginMainMenuBar()) {
ImGui.Text("NET-NES");
ImGui.Separator();
if (ImGui.BeginMenu("File")) {
if (ImGui.MenuItem("Open ROM")) {
fileDialog.Open();
Helper.showMenuBar = false;
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Config")) {
if (ImGui.MenuItem("Window Size")) {
showScaleWindow = true;
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Debug")) {
if (ImGui.MenuItem("FPS Enable", null, Helper.fpsEnable)) {
Helper.fpsEnable = !Helper.fpsEnable;
}
if (ImGui.MenuItem("Sprite0 Hit Disable", null, Helper.debugs0h)) {
Helper.debugs0h = !Helper.debugs0h;
Console.WriteLine("Sprite0 Hit Check Disable: " + Helper.debugs0h);
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Help")) {
if (ImGui.MenuItem("Manual")) {
showManualWindow = true;
}
if (ImGui.MenuItem("About")) {
showAboutWindow = true;
}
ImGui.EndMenu();
}
}
} else {
Helper.showMenuBar = ImGui.GetMousePos().Y <= 20.0f && ImGui.GetMousePos().Y != 0;
}
ImGui.SetNextWindowPos(new System.Numerics.Vector2(0, 0), ImGuiCond.Appearing);
ImGui.SetNextWindowSize(new System.Numerics.Vector2(256 * Helper.scale, 240 * Helper.scale), ImGuiCond.Appearing);
if (fileDialog.Show(ref selectedFilePath)) {
Helper.romPath = selectedFilePath;
Helper.insertingRom = true;
}
ScaleWindow();
ManualWindow();
AboutWindow();
}
public void ScaleWindow() {
if (showScaleWindow) {
ImGui.SetNextWindowPos(new System.Numerics.Vector2(0, 20), ImGuiCond.Appearing);
ImGui.SetNextWindowSize(new System.Numerics.Vector2(125 * 2, 50 * 2), ImGuiCond.Appearing);
if (ImGui.Begin("Window Size Config", ref showScaleWindow)) {
ImGui.SliderInt("Scale", ref Helper.scale, 1, 10);
ImGui.Spacing();
if (ImGui.Button("Close")) {
showScaleWindow = false;
}
}
ImGui.End();
}
}
public void AboutWindow() {
if (showAboutWindow) {
ImGui.SetNextWindowPos(new System.Numerics.Vector2(0, 20), ImGuiCond.Appearing);
ImGui.SetNextWindowSize(new System.Numerics.Vector2(125 * 2, 120 * 2), ImGuiCond.Appearing);
if (ImGui.Begin("About", ref showAboutWindow)) {
ImGui.Text("NET-NES");
ImGui.Text(Helper.version.ToString());
ImGui.Text("Made by Bot Randomness :)");
ImGui.Text(" ____________________________ ");
ImGui.Text("| | NES |---| |");
ImGui.Text("| |____________________|___| |");
ImGui.Text("|____________________________|");
ImGui.Text("| 1 2 |");
ImGui.Text(" \\ O [ ] [ ] D D / ");
ImGui.Text(" *------------------------* ");
if (ImGui.Button("Close")) {
showAboutWindow = false;
}
}
ImGui.End();
}
}
public void ManualWindow() {
if (showManualWindow) {
ImGui.SetNextWindowPos(new System.Numerics.Vector2(0, 20), ImGuiCond.Appearing);
ImGui.SetNextWindowSize(new System.Numerics.Vector2(125 * 2, 120 * 2), ImGuiCond.Appearing);
if (ImGui.Begin("Manual", ref showManualWindow)) {
ImGui.Text("Controls: (A)=X, (B)=Z, \nD-Pad=ArrowKeys, [START]=ENTER, \n[SELECT]=SHIFT");
ImGui.Spacing();
ImGui.Text("Press [SPACE] to toggle the Menu \nBar.");
ImGui.Spacing();
ImGui.Text("In Debug: Toggle Sprite0 Hit Check. \nTry this out if a game freezes");
ImGui.Spacing();
ImGui.Text("Only Catridge Mapper ID Supported: \n0, 1, 2, 4");
if (ImGui.Button("Close")) {
showManualWindow = false;
}
}
ImGui.End();
}
}
class FileDialog {
/*
* File Dialog for ImGui.NET
* This is a simple file dialog, it's flexible and expandable
* This can also easily be ported to other languages for other bindings or the original Dear ImGui
*
* Basic Usage:
* FileDialog fileDialog = new FileDialog();
*
* string selectedFilePath = "";
*
* fileDialog.Open(); //Trigger the file dialog to open
*
* if (fileDialog.Show(ref selectedFilePath)) {
* //Do something with string path
* }
*
* Made by Bot Randomness
*/
private string currentDirectory;
private string selectedFilePath = "";
private bool isOpen = false;
private bool canCancel = true;
public FileDialog(string startDirectory, bool canCancel = true) {
currentDirectory = Directory.Exists(startDirectory) ? startDirectory : Directory.GetCurrentDirectory();
this.canCancel = canCancel;
}
public bool Show(ref string resultFilePath) {
if (!isOpen) return false;
bool fileSelected = false;
if (ImGui.Begin("File Dialog", ImGuiWindowFlags.HorizontalScrollbar)) {
ImGui.Text("Select a file:");
string[] directories = Directory.GetDirectories(currentDirectory);
string[] files = Directory.GetFiles(currentDirectory, "*.*");
ImGui.InputText("File Path", ref selectedFilePath, 260);
if (ImGui.Button("Select File")) {
if (File.Exists(selectedFilePath)) {
resultFilePath = selectedFilePath;
fileSelected = true;
isOpen = false;
}
}
ImGui.SameLine();
if (canCancel) {
if (ImGui.Button("Cancel")) {
isOpen = false;
}
}
if (Path.GetPathRoot(currentDirectory) != currentDirectory) {
if (ImGui.Button("Back")) {
currentDirectory = Directory.GetParent(currentDirectory)?.FullName ?? currentDirectory;
}
}
foreach (var dir in directories) {
if (ImGui.Selectable("[DIR] " + Path.GetFileName(dir))) {
currentDirectory = dir;
}
}
foreach (var file in files) {
if (ImGui.Selectable(Path.GetFileName(file))) {
selectedFilePath = file;
}
}
if (directories.Length == 0 && files.Length == 0) {
ImGui.Text("No files or folders found.");
}
}
ImGui.End();
return fileSelected;
}
public void Open() {
isOpen = true;
}
}
}
#pragma warning restore
================================================
FILE: src/Helper.cs
================================================
public class Helper {
public static int scale = 2;
public static string romPath = "";
public static bool debug = false;
public static bool debugs0h = false;
public static bool fpsEnable = false;
public static bool raylibLog = false;
public static int mode = 1;
public static string jsonPath = "";
public static int flagArraySize = -1;
public static bool insertingRom = false;
public static bool showMenuBar = true;
public static Version version = new Version(1, 0, 0);
public static void Flags(string[] args) {
flagArraySize = args.Length;
if (args.Length >= 1) {
for (int i = 0; i < args.Length; i++) {
if (args[i] == "--nes") {
if (i + 1 < args.Length) {
romPath = args[i + 1];
if (!File.Exists(romPath)) {
Console.WriteLine("ROM path \"" + romPath + "\" is invalid");
Environment.Exit(1);
}
insertingRom = true;
mode = 1;
i += 1;
} else {
Console.WriteLine("No ROM passed in");
Console.WriteLine("Usage: --nes <string:rom>");
//Environment.Exit(1);
}
}
if (args[i] == "--json") {
if (i + 1 < args.Length) {
jsonPath = args[i + 1];
i += 1;
}
mode = 2;
}
if (args[i] == "-s" || args[i] == "--scale") {
if (i + 1 < args.Length && int.TryParse(args[i + 1], out int parsedScale)) {
scale = parsedScale;
i += 1;
} else {
Console.WriteLine("No scale integer passed in");
Console.WriteLine("Usage: -s <int:scale>, --scale <int:scale>");
Environment.Exit(1);
}
}
if (args[i] == "-f" || args[i] == "--fps") {
fpsEnable = true;
}
if (args[i] == "-rl" || args[i] == "-raylib-log") {
raylibLog = true;
}
if (args[i] == "-d" || args[i] == "--debug") {
debug = true;
Console.WriteLine("Press [SPACE] to toggle Sprite0 Hit Check");
}
if (args[i] == "-v" || args[i] == "--version") {
Console.WriteLine(version);
Console.WriteLine("Made by Bot Randomness :)");
ASCII_NES();
Environment.Exit(1);
}
if (args[i] == "-a" || args[i] == "--about") {
Console.WriteLine("Made by Bot Randomness :)");
Console.WriteLine(version);
ASCII_NES();
Environment.Exit(1);
}
if (args[i] == "-h" || args[i] == "--help") {
Console.WriteLine("NES Help:");
Console.WriteLine("--nes <string:rom>: Start up the emulator with given ROM passed in. Consider as mode 1");
Console.WriteLine("--json <string:json>: Runs the JSON Test. The tests must be in \"test\\v1\". Consider as mode 2");
Console.WriteLine("-s <int>, --scale <int>: Scale window size by factor (2 is default)");
Console.WriteLine("-f, --fps: Enables FPS counter (off is default)");
Console.WriteLine("-rl, --raylib-log: Enables Raylib logs (off is default)");
Console.WriteLine("-d, --debug: Enables debug mode (off is default)");
Console.WriteLine("-v, --version: Shows version number");
Console.WriteLine("-a, --about: Show about screen");
Console.WriteLine("-h, --help: Show help screen (What you are seeing now)");
Console.WriteLine("Controls: (A)=X, (B)=Z, D-Pad=ArrowKeys, [START]=ENTER, [SELECT]=SHIFT");
Console.WriteLine("In debug mode: Press [SPACE] to toggle Sprite0 Hit Check. Try this out if a game freezes");
Console.WriteLine("ROMs must have a iNES header!");
Console.WriteLine("In GUI, look at Help -> Manual");
Environment.Exit(1);
}
}
if (mode == 0) {
Console.WriteLine("Error: No mode passed in");
Console.WriteLine("Mode: --nes <string:rom> or --json <string:json>");
Console.WriteLine("Use -h or --help to bring up help options.");
Environment.Exit(1);
}
} else {
Console.WriteLine("To get started, use -h or --help to bring up help options.");
Console.WriteLine("Or use the GUI. \"Help -> Manual\"");
//Environment.Exit(1);
}
Console.WriteLine();
}
public static void ASCII_NES() {
Console.WriteLine(" ____________________________ ");
Console.WriteLine("│ │ NES │---│ │");
Console.WriteLine("│ │____________________│___│ │");
Console.WriteLine("│____________________________│");
Console.WriteLine("| 1 2 |");
Console.WriteLine(" \\ ■ [ ] [ ] ▒ ▒ / ");
Console.WriteLine(" ∙------------------------∙ ");
}
}
================================================
FILE: src/Input.cs
================================================
using Raylib_cs;
public class Input {
private byte controllerState = 0;
private byte controllerShift = 0;
public void UpdateController() {
controllerState = 0;
if (Raylib.IsKeyDown(KeyboardKey.X)) controllerState |= 1 << 0; // A
if (Raylib.IsKeyDown(KeyboardKey.Z)) controllerState |= 1 << 1; // B
if (Raylib.IsKeyDown(KeyboardKey.RightShift) || Raylib.IsKeyDown(KeyboardKey.LeftShift)) controllerState |= 1 << 2; // Select
if (Raylib.IsKeyDown(KeyboardKey.Enter)) controllerState |= 1 << 3; // Start
if (Raylib.IsKeyDown(KeyboardKey.Up)) controllerState |= 1 << 4; // Up
if (Raylib.IsKeyDown(KeyboardKey.Down)) controllerState |= 1 << 5; // Down
if (Raylib.IsKeyDown(KeyboardKey.Left)) controllerState |= 1 << 6; // Left
if (Raylib.IsKeyDown(KeyboardKey.Right)) controllerState |= 1 << 7; // Right
}
public void Write4016(byte value) {
if ((value & 1) != 0) {
controllerShift = controllerState;
}
}
public byte Read4016() {
byte result = (byte)(controllerShift & 1);
controllerShift >>= 1;
return result;
}
}
================================================
FILE: src/NES.cs
================================================
public class NES {
Cartridge cartridge;
Bus bus;
public NES() {
cartridge = new Cartridge(Helper.romPath);
bus = new Bus(cartridge);
bus.cpu.Reset();
Console.WriteLine("NES");
}
public void Run() {
int cycles = 0;
bus.input.UpdateController();
while (cycles < 29828) {
int used = bus.cpu.ExecuteInstruction();
cycles += used;
bus.ppu.Step(used * 3);
}
bus.ppu.DrawFrame(Helper.scale);
}
}
================================================
FILE: src/PPU.cs
================================================
using Raylib_cs;
public class PPU {
private Bus bus;
private byte[] vram; //2KB VRAM
private byte[] paletteRAM; //32 bytes Palette RAM
private byte[] oam; //256 bytes OAM
private const int ScreenWidth = 256;
private const int ScreenHeight = 240;
private const int CyclesPerScanlines = 341;
private const int TotalScanlines = 262;
private byte PPUCTRL; //$2000
private byte PPUMASK; //$2001
private byte PPUSTATUS; //$2002
private byte OAMADDR; //$2003
private byte OAMDATA; //$2004
private byte PPUSCROLLX, PPUSCROLLY; //$2005
private ushort PPUADDR; //$2006
private byte PPUDATA; //$2007
private bool addrLatch = false;
private byte ppuDataBuffer;
private byte fineX; //x
private bool scrollLatch; //w
private ushort v; //current VRAM address
private ushort t; //temp VRAM address
private int scanlineCycle;
private int scanline;
private Image image;
private Texture2D texture;
public int textureX = 0;
public int textureY = 0;
private Color[] frameBuffer;
Color[] scanlineBuffer = new Color[ScreenWidth];
public PPU(Bus bus) {
this.bus = bus;
vram = new byte[2048];
paletteRAM = new byte[32];
oam = new byte[256];
image = Raylib.GenImageColor(ScreenWidth, ScreenHeight, Color.Black);
texture = Raylib.LoadTextureFromImage(image);
frameBuffer = new Color[ScreenWidth * ScreenHeight];
PPUADDR = 0x0000;
PPUCTRL = 0x00;
PPUSTATUS = 0x00;
PPUMASK = 0x00;
ppuDataBuffer = 0x00;
scanlineCycle = 0;
scanline = 0;
Console.WriteLine("PPU init");
}
public void Step(int elapsedCycles) {
for (int i = 0; i < elapsedCycles; i++) {
if (scanline == 0 && scanlineCycle == 0) {
PPUSTATUS &= 0x3F;
}
if (scanline >= 0 && scanline < 240 && scanlineCycle == 260) {
if ((PPUMASK & 0x18) != 0 && bus.cartridge.mapper is Mapper4) {
Mapper4 mmc3 = (Mapper4)bus.cartridge.mapper;
mmc3.RunScanlineIRQ();
if (mmc3.IRQPending()) {
bus.cpu.RequestIRQ(true);
mmc3.ClearIRQ();
}
}
}
scanlineCycle++;
if (scanlineCycle >= 341) {
scanlineCycle = 0;
if (scanline >= 0 && scanline < 240) {
CopyXFromTToV();
RenderScanline(scanline);
IncrementY();
}
if (scanline == 241) {
PPUSTATUS |= 0x80;
if ((PPUCTRL & 0x80) != 0) {
bus.cpu.RequestNMI();
}
}
if (scanline == 261) {
v = t;
}
scanline++;
if (scanline == TotalScanlines) {
scanline = 0;
}
}
}
}
bool[] bgMask = new bool[ScreenWidth];
private void RenderScanline(int scanline) {
Array.Clear(scanlineBuffer, 0, ScreenWidth);
Array.Clear(bgMask, 0, ScreenWidth);
RenderBackground(bgMask);
RenderSprite(bgMask);
Array.Copy(scanlineBuffer, 0, frameBuffer, scanline * ScreenWidth, ScreenWidth);
}
public void RenderBackground(bool[] bgMask) {
if ((PPUMASK & 0x08) == 0) return;
ushort renderV = v;
for (int tile = 0; tile < 33; tile++) {
int coarseX = renderV & 0x001F;
int coarseY = (renderV >> 5) & 0x001F;
int nameTable = (renderV >> 10) & 0x0003;
int baseNTAddr = 0x2000 + (nameTable * 0x400);
int tileAddr = baseNTAddr + (coarseY * 32) + coarseX;
byte tileIndex = Read((ushort)tileAddr);
int fineY = (renderV >> 12) & 0x7;
int patternTable = (PPUCTRL & 0x10) != 0 ? 0x1000 : 0x0000;
int patternAddr = patternTable + (tileIndex * 16) + fineY;
byte plane0 = Read((ushort)patternAddr);
byte plane1 = Read((ushort)(patternAddr + 8));
int attributeX = coarseX / 4;
int attributeY = coarseY / 4;
int attrAddr = baseNTAddr + 0x3C0 + attributeY * 8 + attributeX;
byte attrByte = Read((ushort)attrAddr);
int attrShift = ((coarseY % 4) / 2) * 4 + ((coarseX % 4) / 2) * 2;
int paletteIndex = (attrByte >> attrShift) & 0x03;
for (int i = 0; i < 8; i++) {
int pixel = tile * 8 + i - fineX;
if (pixel < 0 || pixel >= ScreenWidth) continue;
int bitIndex = 7 - i;
int bit0 = (plane0 >> bitIndex) & 1;
int bit1 = (plane1 >> bitIndex) & 1;
int colorIndex = bit0 | (bit1 << 1);
if (colorIndex != 0) bgMask[pixel] = true;
scanlineBuffer[pixel] = GetColorFromPalette(colorIndex, paletteIndex);
}
IncrementX(ref renderV);
}
}
public void RenderSprite(bool[] bgMask) {
bool showSprites = (PPUMASK & 0x10) != 0;
if (showSprites) {
bool isSprite8x16 = (PPUCTRL & 0x20) != 0;
bool[] spritePixelDrawn = new bool[ScreenWidth];
for (int i = 0; i < 64; i++) {
int offset = i * 4;
byte spriteY = oam[offset];
byte tileIndex = oam[offset + 1];
byte attributes = oam[offset + 2];
byte spriteX = oam[offset + 3];
int paletteIndex = attributes & 0b11;
bool flipX = (attributes & 0x40) != 0;
bool flipY = (attributes & 0x80) != 0;
bool priority = (attributes & 0x20) == 0;
int tileHeight = isSprite8x16 ? 16 : 8;
if (scanline < spriteY || scanline >= spriteY + tileHeight)
continue;
int subY = scanline - spriteY;
if (flipY) subY = tileHeight - 1 - subY;
int subTileIndex = isSprite8x16 ? (tileIndex & 0xFE) + (subY / 8) : tileIndex;
int patternTable = isSprite8x16
? ((tileIndex & 1) != 0 ? 0x1000 : 0x0000)
: ((PPUCTRL & 0x08) != 0 ? 0x1000 : 0x0000);
int baseAddr = patternTable + subTileIndex * 16;
byte plane0 = Read((ushort)(baseAddr + (subY % 8)));
byte plane1 = Read((ushort)(baseAddr + (subY % 8) + 8));
for (int x = 0; x < 8; x++) {
int bit = flipX ? x : 7 - x;
int bit0 = (plane0 >> bit) & 1;
int bit1 = (plane1 >> bit) & 1;
int color = bit0 | (bit1 << 1);
if (color == 0) continue;
int px = spriteX + x;
if (px < 0 || px >= ScreenWidth) continue;
//Sprite 0 hit detection
if (i == 0 && bgMask[px] && color != 0 && Helper.debugs0h == false) {
PPUSTATUS |= 0x40;
} else if (Helper.debugs0h == true) { //Debug to skip check for Sprite0 Hit
PPUSTATUS |= 0x40;
}
if (spritePixelDrawn[px]) continue;
bool shouldDraw = true;
if (!priority && bgMask[px]) {
shouldDraw = false;
}
if (shouldDraw) {
scanlineBuffer[px] = GetSpriteColor(color, paletteIndex);
spritePixelDrawn[px] = true;
}
}
}
}
}
public void WritePPURegister(ushort address, byte value) {
switch (address) {
case 0x2000:
PPUCTRL = value;
t = (ushort)((t & 0xF3FF) | ((value & 0x03) << 10));
break;
case 0x2001:
PPUMASK = value;
break;
case 0x2002:
PPUSTATUS &= 0x7F;
scrollLatch = false;
break;
case 0x2003:
OAMADDR = value;
break;
case 0x2004:
OAMDATA = value;
oam[OAMADDR++] = OAMDATA;
break;
case 0x2005:
if (!scrollLatch) {
PPUSCROLLX = value;
fineX = (byte)(value & 0x07);
t = (ushort)((t & 0xFFE0) | (value >> 3));
} else {
PPUSCROLLY = value;
t = (ushort)((t & 0x8FFF) | ((value & 0x07) << 12));
t = (ushort)((t & 0xFC1F) | ((value & 0xF8) << 2));
}
scrollLatch = !scrollLatch;
break;
case 0x2006:
if (!addrLatch) {
t = (ushort)((value << 8) | (t & 0x00FF));
PPUADDR = t;
} else {
t = (ushort)((t & 0xFF00) | value);
PPUADDR = t;
v = t;
}
addrLatch = !addrLatch;
break;
case 0x2007:
PPUDATA = value;
Write(PPUADDR, PPUDATA);
PPUADDR += ((PPUCTRL & 0x04) != 0) ? (ushort)32 : (ushort)1;
v = PPUADDR;
break;
}
}
public byte ReadPPURegister(ushort address) {
byte result = 0x00;
switch (address) {
case 0x2000:
result = PPUCTRL;
break;
case 0x2002:
result = PPUSTATUS;
PPUSTATUS &= 0x3F;
addrLatch = false;
break;
case 0x2004:
result = oam[OAMADDR];
break;
case 0x2006:
result = (byte)(PPUADDR >> 8);
break;
case 0x2007:
result = ppuDataBuffer;
ppuDataBuffer = Read(PPUADDR);
if (PPUADDR >= 0x3F00) {
result = ppuDataBuffer;
}
PPUADDR += ((PPUCTRL & 0x04) != 0) ? (ushort)32 : (ushort)1;
return result;
}
return result;
}
public byte Read(ushort address) {
address = (ushort)(address & 0x3FFF);
if (address < 0x2000) {
return bus.cartridge.PPURead(address);
} else if (address >= 0x2000 && address <= 0x3EFF) {
ushort mirrored = MirrorVRAMAddress(address);
return vram[mirrored];
} else if (address >= 0x3F00 && address <= 0x3FFF) {
ushort mirrored = (ushort)(address & 0x1F);
if (mirrored >= 0x10 && (mirrored % 4) == 0) mirrored -= 0x10;
return paletteRAM[mirrored];
}
return 0;
}
public void Write(ushort address, byte value) {
address = (ushort)(address & 0x3FFF);
if (address < 0x2000) {
bus.cartridge.PPUWrite(address, value);
} else if (address >= 0x2000 && address <= 0x3EFF) {
ushort mirrored = MirrorVRAMAddress(address);
vram[mirrored] = value;
} else if (address >= 0x3F00 && address <= 0x3FFF) {
ushort mirrored = (ushort)(address & 0x1F);
if (mirrored >= 0x10 && (mirrored % 4) == 0) mirrored -= 0x10;
paletteRAM[mirrored] = value;
}
}
private ushort MirrorVRAMAddress(ushort address) {
ushort offset = (ushort)(address & 0x0FFF);
int ntIndex = offset / 0x400;
int innerOffset = offset % 0x400;
switch (bus.cartridge.mirroringMode) {
case Mirroring.Vertical:
return (ushort)((ntIndex % 2) * 0x400 + innerOffset);
case Mirroring.Horizontal:
return (ushort)(((ntIndex / 2) * 0x400) + innerOffset);
case Mirroring.SingleScreenA:
return (ushort)(innerOffset);
case Mirroring.SingleScreenB:
return (ushort)(0x400 + innerOffset);
default:
return offset;
}
}
public void WriteOAMDMA(byte page) {
ushort baseAddr = (ushort)(page << 8);
for (int i = 0; i < 256; i++) {
byte value = bus.Read((ushort)(baseAddr + i));
oam[OAMADDR++] = value;
}
}
private void IncrementY() {
if ((v & 0x7000) != 0x7000) {
v += 0x1000;
} else {
v &= 0x8FFF;
int y = (v & 0x03E0) >> 5;
if (y == 29) {
y = 0;
v ^= 0x0800;
} else if (y == 31) {
y = 0;
} else {
y += 1;
}
v = (ushort)((v & 0xFC1F) | (y << 5));
}
}
private void IncrementX(ref ushort addr) {
if ((addr & 0x001F) == 31) {
addr &= 0xFFE0;
addr ^= 0x0400;
} else {
addr++;
}
}
private void CopyXFromTToV() {
v = (ushort)((v & 0xFBE0) | (t & 0x041F));
}
private Color GetSpriteColor(int colorIndex, int paletteIndex) {
int paletteBase = 0x11 + paletteIndex * 4;
byte paletteColor = paletteRAM[paletteBase + (colorIndex - 1)];
return NesPalette[paletteColor % 64];
}
private Color GetColorFromPalette(int colorIndex, int paletteIndex) {
if (colorIndex == 0) {
byte bgColorIndex = paletteRAM[0];
return NesPalette[bgColorIndex % 64];
}
int paletteBase = 1 + (paletteIndex * 4);
byte paletteColorIndex = paletteRAM[(paletteBase + colorIndex - 1) % 32];
return NesPalette[paletteColorIndex % 64];
}
public void DrawFrame(int scale) {
for (int y = 0; y < ScreenHeight; y++) {
for (int x = 0; x < ScreenWidth; x++) {
Color color = frameBuffer[y * ScreenWidth + x];
Raylib.ImageDrawPixel(ref image, x, y, color);
}
}
unsafe {
Raylib.UpdateTexture(texture, image.Data);
}
Raylib.DrawTextureEx(texture, new System.Numerics.Vector2(textureX, textureY), 0.0f, scale, Color.White);
}
//NES 64 Color Palette
static readonly Color[] NesPalette = new Color[] {
new Color(84, 84, 84, 255), new Color(0, 30, 116, 255), new Color(8, 16, 144, 255), new Color(48, 0, 136, 255),
new Color(68, 0, 100, 255), new Color(92, 0, 48, 255), new Color(84, 4, 0, 255), new Color(60, 24, 0, 255),
new Color(32, 42, 0, 255), new Color(8, 58, 0, 255), new Color(0, 64, 0, 255), new Color(0, 60, 0, 255),
new Color(0, 50, 60, 255), new Color(0, 0, 0, 255), new Color(0, 0, 0, 255), new Color(0, 0, 0, 255),
new Color(152, 150, 152, 255), new Color(8, 76, 196, 255), new Color(48, 50, 236, 255), new Color(92, 30, 228, 255),
new Color(136, 20, 176, 255), new Color(160, 20, 100, 255), new Color(152, 34, 32, 255), new Color(120, 60, 0, 255),
new Color(84, 90, 0, 255), new Color(40, 114, 0, 255), new Color(8, 124, 0, 255), new Color(0, 118, 40, 255),
new Color(0, 102, 120, 255), new Color(0, 0, 0, 255), new Color(0, 0, 0, 255), new Color(0, 0, 0, 255),
new Color(236, 238, 236, 255), new Color(76, 154, 236, 255), new Color(120, 124, 236, 255),new Color(176, 98, 236, 255),
new Color(228, 84, 236, 255), new Color(236, 88, 180, 255), new Color(236, 106, 100, 255),new Color(212, 136, 32, 255),
new Color(160, 170, 0, 255), new Color(116, 196, 0, 255), new Color(76, 208, 32, 255), new Color(56, 204, 108, 255),
new Color(56, 180, 204, 255), new Color(60, 60, 60, 255), new Color(0, 0, 0, 255), new Color(0, 0, 0, 255),
new Color(236, 238, 236, 255), new Color(168, 204, 236, 255),new Color(188, 188, 236, 255),new Color(212, 178, 236, 255),
new Color(236, 174, 236, 255), new Color(236, 174, 212, 255),new Color(236, 180, 176, 255),new Color(228, 196, 144, 255),
new Color(204, 210, 120, 255), new Color(180, 222, 120, 255),new Color(168, 226, 144, 255),new Color(152, 226, 180, 255),
new Color(160, 214, 228, 255), new Color(160, 162, 160, 255),new Color(0, 0, 0, 255), new Color(0, 0, 0, 255)
};
}
================================================
FILE: src/Program.cs
================================================
public class Program {
public static void Main(string[] args) {
Console.WriteLine("NET-NES");
Helper.Flags(args);
if (Helper.mode == 1) {
GUI gui = new GUI();
gui.Run();
} else if (Helper.mode == 2) {
TestRunner testRunner = new TestRunner();
testRunner.Run(Helper.jsonPath);
}
}
}
================================================
FILE: src/Test.cs
================================================
using Newtonsoft.Json;
class JSONTest {
public class ProcessorState {
public int pc { get; set; }
public int s { get; set; }
public int a { get; set; }
public int x { get; set; }
public int y { get; set; }
public int p { get; set; }
public List<List<int>> ram { get; set; } = new List<List<int>>();
}
public class Test {
public string name { get; set; } = "";
public ProcessorState initial { get; set; } = new ProcessorState();
public ProcessorState final { get; set; } = new ProcessorState();
public List<List<object>> cycles { get; set; } = new List<List<object>>();
}
public IBus bus;
public CPU cpu;
public JSONTest() {
bus = new TestBus();
cpu = new CPU(bus);
}
public void Run(string jsonPath) {
string filePath = jsonPath;
var json = File.ReadAllText(filePath);
var tests = JsonConvert.DeserializeObject<List<Test>>(json) ?? new List<Test>();
foreach (var test in tests) {
Console.WriteLine(test.name);
cpu.PC = (ushort)test.initial.pc;
cpu.SP = (ushort)test.initial.s;
cpu.A = (byte)test.initial.a;
cpu.X = (byte)test.initial.x;
cpu.Y = (byte)test.initial.y;
cpu.status = (byte)test.initial.p;
string initCPU16Reg = $"PC: {cpu.PC}, SP: {cpu.SP}";
string initCPUReg = $"A: {cpu.A}, X: {cpu.X}, Y: {cpu.Y}, P: {cpu.status}";
string initRAM = "";
foreach (var entry in test.initial.ram) {
bus.Write((ushort)entry[0], (byte)entry[1]);
initRAM += $"Address: {entry[0]}, Value: {entry[1]}\n";
}
int actualCycleCount = cpu.ExecuteInstruction();
string finalCPU16Reg = $"PC: {cpu.PC}, SP: {cpu.SP}";
string finalCPUReg = $"A: {cpu.A}, X: {cpu.X}, Y: {cpu.Y}, P: {cpu.status}";
string finalRAM = "";
bool isMismatch = false;
if (cpu.A != test.final.a) { Console.WriteLine($"Mismatch in A: Expected {test.final.a}, Found {cpu.A}"); isMismatch = true; }
if (cpu.X != test.final.x) { Console.WriteLine($"Mismatch in X: Expected {test.final.x}, Found {cpu.X}"); isMismatch = true; }
if (cpu.Y != test.final.y) { Console.WriteLine($"Mismatch in Y: Expected {test.final.y}, Found {cpu.Y}"); isMismatch = true; }
if (cpu.status != test.final.p) { Console.WriteLine($"Mismatch in P: Expected {test.final.p}, Found {cpu.status}"); isMismatch = true; }
if (cpu.PC != test.final.pc) { Console.WriteLine($"Mismatch in Pc: Expected {test.final.pc}, Found {cpu.PC}"); isMismatch = true; }
if (cpu.SP != test.final.s) { Console.WriteLine($"Mismatch in Sp: Expected {test.final.s}, Found {cpu.SP}"); isMismatch = true; }
foreach (var entry in test.final.ram) {
int valueInMMU = bus.Read((ushort)entry[0]);
finalRAM += $"Address: {entry[0]}, Value: {entry[1]}\n";
if (valueInMMU != entry[1]) {
Console.WriteLine($"Mismatch in RAM at Address {entry[0]}: Expected {entry[1]}, Found {valueInMMU}");
isMismatch = true;
}
}
int expectedCycleCount = test.cycles.Count;
if (expectedCycleCount != actualCycleCount) {
Console.WriteLine($"Mismatch in cycles: Expected {expectedCycleCount}, Found {actualCycleCount}");
isMismatch = true;
}
if (isMismatch) {
//To compare init and final values to JSON for full detail if init properly or anyother
Console.WriteLine("\nCPU and RAM init:");
Console.WriteLine(initCPU16Reg);
Console.WriteLine(initCPUReg);
Console.WriteLine(initRAM);
Console.WriteLine("CPU and RAM final:");
Console.WriteLine(finalCPU16Reg);
Console.WriteLine(finalCPUReg);
Console.WriteLine(finalRAM);
Console.WriteLine("JSON Test:");
string testJson = JsonConvert.SerializeObject(test, Formatting.Indented);
Console.WriteLine(testJson);
Environment.Exit(1);
}
}
Console.WriteLine("All tests passed!");
}
}
================================================
FILE: src/TestBus.cs
================================================
public class TestBus : IBus {
public byte[] ram; //64 KB RAM
public TestBus() {
ram = new byte[65536];
Console.WriteLine("Test Bus init");
}
public void Write(ushort address, byte value) {
ram[address] = value;
}
public byte Read(ushort address) {
return ram[address];
}
}
================================================
FILE: src/TestRunner.cs
================================================
public class TestRunner {
public TestRunner() {
}
public void Run(string test) {
if (test != "all") {
if (!File.Exists(Path.Combine("test", "v1", test))) {
Console.WriteLine("Test file \"" + Path.Combine("test", "v1", test) + "\" does not exist");
Console.WriteLine("Provide JSON test file, or to test all, pass in \"all\"");
Environment.Exit(1);
}
JSONTest jsonTest = new JSONTest();
jsonTest.Run(Path.Combine("test", "v1", test));
} else {
if (!Directory.Exists(Path.Combine("test", "v1"))) {
Console.WriteLine("Could not find directory \"" + Path.Combine("test", "v1") + "\"");
Console.WriteLine("Provide JSON test file, or to test all, pass in \"all\"");
Environment.Exit(1);
}
string[] testFiles = Directory.GetFiles(Path.Combine("test", "v1"));
string[] skipArray = {
"80.json", "89.json", "9e.json",
"02.json", "03.json", "04.json", "07.json", "xx.json", "0b.json", "0c.json", "0f.json",
"12.json", "13.json", "14.json", "17.json", "1a.json", "1b.json", "1c.json", "1f.json",
"22.json", "23.json", "xx.json", "27.json", "xx.json", "2b.json", "xx.json", "2f.json",
"32.json", "33.json", "34.json", "37.json", "3a.json", "3b.json", "3c.json", "3f.json",
"42.json", "43.json", "44.json", "47.json", "xx.json", "4b.json", "xx.json", "4f.json",
"52.json", "53.json", "54.json", "57.json", "5a.json", "5b.json", "5c.json", "5f.json",
"62.json", "63.json", "64.json", "67.json", "xx.json", "6b.json", "xx.json", "6f.json",
"72.json", "73.json", "74.json", "77.json", "7a.json", "7b.json", "7c.json", "7f.json",
"82.json", "83.json", "xx.json", "87.json", "xx.json", "8b.json", "xx.json", "8f.json",
"92.json", "93.json", "xx.json", "97.json", "xx.json", "9b.json", "9c.json", "9f.json",
"xx.json", "a3.json", "xx.json", "a7.json", "xx.json", "ab.json", "xx.json", "af.json",
"b2.json", "b3.json", "xx.json", "b7.json", "xx.json", "bb.json", "xx.json", "bf.json",
"c2.json", "c3.json", "xx.json", "c7.json", "xx.json", "cb.json", "xx.json", "cf.json",
"d2.json", "d3.json", "d4.json", "d7.json", "da.json", "db.json", "dc.json", "df.json",
"e2.json", "e3.json", "xx.json", "e7.json", "xx.json", "eb.json", "xx.json", "ef.json",
"f2.json", "f3.json", "f4.json", "f7.json", "fa.json", "fb.json", "fc.json", "ff.json"
};
using StreamWriter log = new StreamWriter("log.txt", append: false) {AutoFlush = true};
int tested = 0;
int skipped = 0;
JSONTest jsonTest = new JSONTest();
foreach (string filePath in testFiles) {
if (skipArray.Contains(Path.GetFileName(filePath), StringComparer.OrdinalIgnoreCase)) {
Console.WriteLine($"Skipping test: {Path.GetFileName(filePath)}");
log.WriteLine($"Skipping test: {Path.GetFileName(filePath)}");
skipped += 1;
continue;
}
Console.WriteLine($"Running test: {Path.GetFileName(filePath)}");
log.WriteLine($"Running test: {Path.GetFileName(filePath)}");
tested += 1;
jsonTest.Run(filePath);
}
Console.WriteLine($"Number of Tested Opcodes: {tested}, Number of Skipped Opcodes: {skipped}");
log.WriteLine($"Number of Tested Opcodes: {tested}, Number of Skipped Opcodes: {skipped}");
}
}
}
================================================
FILE: src/interface/IBus.cs
================================================
public interface IBus {
void Write(ushort address, byte value);
byte Read(ushort address);
}
================================================
FILE: src/interface/IMapper.cs
================================================
public interface IMapper {
void Reset();
byte CPURead(ushort address);
void CPUWrite(ushort address, byte value);
byte PPURead(ushort address);
void PPUWrite(ushort address, byte value);
}
================================================
FILE: src/mappers/Mapper0.cs
================================================
public class Mapper0 : IMapper { //NROM
private Cartridge cartridge;
public Mapper0(Cartridge cart) {
cartridge = cart;
}
public void Reset() {
}
public byte CPURead(ushort address) {
if (address >= 0x6000 && address <= 0x7FFF) {
return cartridge.prgRAM[address - 0x6000];
} else if (address >= 0x8000 && address <= 0xFFFF) {
if (cartridge.prgBanks == 1) {
return cartridge.prgROM[address & 0x3FFF];
} else {
return cartridge.prgROM[address - 0x8000];
}
}
return 0;
}
public void CPUWrite(ushort address, byte value) {
if (address >= 0x6000 && address <= 0x7FFF) {
cartridge.prgRAM[address - 0x6000] = value;
}
}
public byte PPURead(ushort address) {
if (address < 0x2000) {
if (cartridge.chrBanks != 0) {
return cartridge.chrROM[address];
} else {
return cartridge.chrRAM[address];
}
}
return 0;
}
public void PPUWrite(ushort address, byte value) {
if (address < 0x2000 && cartridge.chrBanks == 0) {
cartridge.chrRAM[address] = value;
}
}
}
================================================
FILE: src/mappers/Mapper1.cs
================================================
public class Mapper1 : IMapper { //MMC1 (Experimenal)
private Cartridge cartridge;
private byte shiftRegister = 0x10;
private byte control = 0x0C;
private byte chrBank0, chrBank1, prgBank;
private int shiftCount = 0;
private int prgBankOffset0, prgBankOffset1;
private int chrBankOffset0, chrBankOffset1;
public Mapper1(Cartridge cart) {
cartridge = cart;
//Reset();
}
public void Reset() {
shiftRegister = 0x10;
control = 0x0C;
chrBank0 = chrBank1 = prgBank = 0;
shiftCount = 0;
ApplyMirroring();
ApplyBanks();
}
public byte CPURead(ushort addr) {
if (addr >= 0x6000 && addr <= 0x7FFF) {
return cartridge.prgRAM[addr - 0x6000];
} else if (addr >= 0x8000 && addr <= 0xBFFF) {
int index = prgBankOffset0 + (addr - 0x8000);
return cartridge.prgROM[index];
} else if (addr >= 0xC000 && addr <= 0xFFFF) {
int index = prgBankOffset1 + (addr - 0xC000);
return cartridge.prgROM[index];
}
return 0;
}
public void CPUWrite(ushort addr, byte val) {
if (addr >= 0x6000 && addr <= 0x7FFF) {
cartridge.prgRAM[addr - 0x6000] = val;
return;
}
if (addr < 0x8000) return;
if ((val & 0x80) != 0) {
shiftRegister = 0x10;
control |= 0x0C;
shiftCount = 0;
ApplyBanks();
return;
}
shiftRegister = (byte)((shiftRegister >> 1) | ((val & 1) << 4));
shiftCount++;
if (shiftCount == 5) {
int reg = (addr >> 13) & 0x03;
switch (reg) {
case 0:
control = (byte)(shiftRegister & 0x1F);
ApplyMirroring();
break;
case 1:
chrBank0 = (byte)(shiftRegister & 0x1F);
ApplyMirroring();
break;
case 2:
chrBank1 = (byte)(shiftRegister & 0x1F);
break;
case 3:
prgBank = (byte)(shiftRegister & 0x0F);
break;
}
shiftRegister = 0x10;
shiftCount = 0;
ApplyBanks();
}
}
public byte PPURead(ushort addr) {
if (addr < 0x2000) {
if (cartridge.chrBanks == 0) {
return cartridge.chrRAM[addr];
}
int chrMode = (control >> 4) & 1;
if (chrMode == 0) {
int offset = (chrBank0 & 0x1E) * 0x1000;
return cartridge.chrROM[(addr + offset) % cartridge.chrROM.Length];
} else {
if (addr < 0x1000) {
return cartridge.chrROM[(addr + chrBankOffset0) % cartridge.chrROM.Length];
} else {
return cartridge.chrROM[((addr - 0x1000) + chrBankOffset1) % cartridge.chrROM.Length];
}
}
}
return 0;
}
public void PPUWrite(ushort addr, byte val) {
if (addr < 0x2000 && cartridge.chrBanks == 0) {
cartridge.chrRAM[addr] = val;
}
}
private void ApplyMirroring() {
switch (control & 0x03) {
case 0: cartridge.SetMirroring(Mirroring.SingleScreenA); break;
case 1: cartridge.SetMirroring(Mirroring.SingleScreenB); break;
case 2: cartridge.SetMirroring(Mirroring.Vertical); break;
case 3: cartridge.SetMirroring(Mirroring.Horizontal); break;
}
}
private void ApplyBanks() {
int chrMode = (control >> 4) & 1;
if (chrMode == 0) {
chrBankOffset0 = (chrBank0 & 0x1E) * 0x1000;
chrBankOffset1 = chrBankOffset0 + 0x1000;
} else {
chrBankOffset0 = chrBank0 * 0x1000;
chrBankOffset1 = chrBank1 * 0x1000;
}
if (cartridge.chrBanks > 0) {
chrBankOffset0 %= cartridge.chrROM.Length;
chrBankOffset1 %= cartridge.chrROM.Length;
}
int prgMode = (control >> 2) & 0x03;
int prgBankCount = cartridge.prgROM.Length / 0x4000;
switch (prgMode) {
case 0:
case 1:
int bank = (prgBank & 0x0E) % Math.Max(1, prgBankCount);
prgBankOffset0 = bank * 0x4000;
prgBankOffset1 = prgBankOffset0 + 0x4000;
break;
case 2:
prgBankOffset0 = 0;
prgBankOffset1 = (prgBank % Math.Max(1, prgBankCount)) * 0x4000;
break;
case 3:
prgBankOffset0 = (prgBank % Math.Max(1, prgBankCount)) * 0x4000;
prgBankOffset1 = (prgBankCount - 1) * 0x4000;
break;
}
prgBankOffset0 %= cartridge.prgROM.Length;
prgBankOffset1 %= cartridge.prgROM.Length;
}
}
================================================
FILE: src/mappers/Mapper2.cs
================================================
public class Mapper2 : IMapper { //UxROM (Experimental)
private Cartridge cartridge;
private byte prgBank;
public Mapper2(Cartridge cart) {
cartridge = cart;
prgBank = 0;
}
public void Reset() {
prgBank = 0;
}
public byte CPURead(ushort addr) {
if (addr >= 0x8000 && addr <= 0xBFFF) {
int index = (prgBank * 0x4000) + (addr - 0x8000);
return index < cartridge.prgROM.Length ? cartridge.prgROM[index] : (byte)0xFF;
} else if (addr >= 0xC000 && addr <= 0xFFFF) {
int fixedBankStart = cartridge.prgROM.Length - 0x4000;
int index = fixedBankStart + (addr - 0xC000);
return index < cartridge.prgROM.Length ? cartridge.prgROM[index] : (byte)0xFF;
}
return 0;
}
public void CPUWrite(ushort addr, byte val) {
if (addr >= 0x8000) {
prgBank = (byte)(val & 0x0F);
}
}
public byte PPURead(ushort addr) {
if (addr < 0x2000) {
if (cartridge.chrBanks == 0)
return cartridge.chrRAM[addr];
return cartridge.chrROM[addr % cartridge.chrROM.Length];
}
return 0;
}
public void PPUWrite(ushort addr, byte val) {
if (cartridge.chrBanks == 0 && addr < 0x2000) {
cartridge.chrRAM[addr] = val;
}
}
}
================================================
FILE: src/mappers/Mapper4.cs
================================================
public class Mapper4 : IMapper { //MMC3 (Experimental)
private Cartridge cartridge;
private byte bankSelect;
private byte[] bankData = new byte[8];
private int[] prgBankOffsets = new int[4];
private int[] chrBankOffsets = new int[8];
private bool prgMode;
private bool chrMode;
private bool prgRamEnable;
private bool prgRamWriteProtect;
private byte irqLatch;
private byte irqCounter;
private bool irqEnable;
private bool irqReloadPending;
private bool irqAsserted;
public Mapper4(Cartridge cart) {
cartridge = cart;
//Reset();
}
public void Reset() {
bankSelect = 0;
for (int i = 0; i < bankData.Length; i++) {
bankData[i] = 0;
}
prgMode = false;
chrMode = false;
prgRamEnable = true;
prgRamWriteProtect = false;
irqLatch = 0;
irqCounter = 0;
irqEnable = false;
irqReloadPending = false;
irqAsserted = false;
ApplyBankMapping();
}
public void RunScanlineIRQ() {
if (irqCounter == 0) {
irqCounter = irqLatch;
} else {
irqCounter--;
if (irqCounter == 0 && irqEnable) {
irqAsserted = true;
}
}
if (irqReloadPending) {
irqCounter = irqLatch;
irqReloadPending = false;
}
}
public bool IRQPending() {
return irqAsserted;
}
public void ClearIRQ() {
irqAsserted = false;
}
public byte CPURead(ushort address) {
if (address >= 0x6000 && address <= 0x7FFF) {
if (prgRamEnable) {
int ramOffset = (address - 0x6000) % cartridge.prgRAM.Length;
return cartridge.prgRAM[ramOffset];
}
return 0xFF;
}
if (address >= 0x8000 && address <= 0xFFFF) {
int bankIndex = (address - 0x8000) / 0x2000;
int bankOffset = prgBankOffsets[bankIndex];
int addressOffset = address % 0x2000;
int finalOffset = (bankOffset + addressOffset) % cartridge.prgROM.Length;
return cartridge.prgROM[finalOffset];
}
return 0;
}
public void CPUWrite(ushort address, byte value) {
if (address >= 0x6000 && address <= 0x7FFF) {
if (prgRamEnable && !prgRamWriteProtect) {
int ramOffset = (address - 0x6000) % cartridge.prgRAM.Length;
cartridge.prgRAM[ramOffset] = value;
}
return;
}
switch (address & 0xE001) {
case 0x8000:
bankSelect = value;
prgMode = (value & 0x40) != 0;
chrMode = (value & 0x80) != 0;
ApplyBankMapping();
break;
case 0x8001:
int reg = bankSelect & 0x07;
bankData[reg] = value;
ApplyBankMapping();
break;
case 0xA000:
if ((value & 1) == 0)
cartridge.SetMirroring(Mirroring.Vertical);
else
cartridge.SetMirroring(Mirroring.Horizontal);
break;
case 0xA001:
prgRamEnable = (value & 0x80) != 0;
prgRamWriteProtect = (value & 0x40) != 0;
break;
case 0xC000:
irqLatch = value;
break;
case 0xC001:
irqReloadPending = true;
break;
case 0xE000:
irqEnable = false;
irqAsserted = false;
break;
case 0xE001:
irqEnable = true;
break;
}
}
public byte PPURead(ushort address) {
if (address >= 0x2000) return 0;
if (cartridge.chrBanks == 0) {
return cartridge.chrRAM[address % cartridge.chrRAM.Length];
}
int bank = address / 0x0400;
int bankOffset = chrBankOffsets[bank];
int addressOffset = address % 0x0400;
int finalOffset = (bankOffset + addressOffset) % cartridge.chrROM.Length;
return cartridge.chrROM[finalOffset];
}
public void PPUWrite(ushort address, byte value) {
if (address < 0x2000) {
if (cartridge.chrBanks == 0) {
cartridge.chrRAM[address] = value;
}
}
}
private void ApplyBankMapping() {
if (chrMode) {
chrBankOffsets[0] = bankData[2] * 0x400;
chrBankOffsets[1] = bankData[3] * 0x400;
chrBankOffsets[2] = bankData[4] * 0x400;
chrBankOffsets[3] = bankData[5] * 0x400;
chrBankOffsets[4] = (bankData[0] & 0xFE) * 0x400;
chrBankOffsets[5] = chrBankOffsets[4] + 0x400;
chrBankOffsets[6] = (bankData[1] & 0xFE) * 0x400;
chrBankOffsets[7] = chrBankOffsets[6] + 0x400;
} else {
chrBankOffsets[0] = (bankData[0] & 0xFE) * 0x400;
chrBankOffsets[1] = chrBankOffsets[0] + 0x400;
chrBankOffsets[2] = (bankData[1] & 0xFE) * 0x400;
chrBankOffsets[3] = chrBankOffsets[2] + 0x400;
chrBankOffsets[4] = bankData[2] * 0x400;
chrBankOffsets[5] = bankData[3] * 0x400;
chrBankOffsets[6] = bankData[4] * 0x400;
chrBankOffsets[7] = bankData[5] * 0x400;
}
int bankCount = cartridge.prgROM.Length / 0x2000;
int lastBank = bankCount - 1;
int bank6 = bankData[6] % bankCount;
int bank7 = bankData[7] % bankCount;
if (prgMode) {
prgBankOffsets[0] = (lastBank - 1) * 0x2000;
prgBankOffsets[1] = bank7 * 0x2000;
prgBankOffsets[2] = bank6 * 0x2000;
prgBankOffsets[3] = lastBank * 0x2000;
} else {
prgBankOffsets[0] = bank6 * 0x2000;
prgBankOffsets[1] = bank7 * 0x2000;
prgBankOffsets[2] = (lastBank - 1) * 0x2000;
prgBankOffsets[3] = lastBank * 0x2000;
}
if (cartridge.chrBanks > 0) {
for (int i = 0; i < 8; i++) {
chrBankOffsets[i] %= cartridge.chrROM.Length;
}
}
for (int i = 0; i < 4; i++) {
prgBankOffsets[i] %= cartridge.prgROM.Length;
}
}
}
================================================
FILE: test/README.txt
================================================
JSON test goes here.
test/v1/XX.json
JSON test repo used from SingleStepTest on GitHub for 6502: https://github.com/SingleStepTests/65x02
gitextract_r7is6r8c/
├── .gitignore
├── LICENSE
├── NET-NES.csproj
├── README.md
├── src/
│ ├── Bus.cs
│ ├── CPU.cs
│ ├── Cartridge.cs
│ ├── GUI.cs
│ ├── Helper.cs
│ ├── Input.cs
│ ├── NES.cs
│ ├── PPU.cs
│ ├── Program.cs
│ ├── Test.cs
│ ├── TestBus.cs
│ ├── TestRunner.cs
│ ├── interface/
│ │ ├── IBus.cs
│ │ └── IMapper.cs
│ └── mappers/
│ ├── Mapper0.cs
│ ├── Mapper1.cs
│ ├── Mapper2.cs
│ └── Mapper4.cs
└── test/
└── README.txt
SYMBOL INDEX (171 symbols across 18 files)
FILE: src/Bus.cs
class Bus (line 1) | public class Bus : IBus{
method Bus (line 10) | public Bus(Cartridge cartridge) {
method Read (line 20) | public byte Read(ushort address) {
method Write (line 38) | public void Write(ushort address, byte value) {
FILE: src/CPU.cs
class CPU (line 1) | public class CPU {
method CPU (line 20) | public CPU(IBus bus) {
method Reset (line 36) | public void Reset() {
method SetFlag (line 46) | public void SetFlag(int bit, bool value) {
method GetFlag (line 54) | public bool GetFlag(int bit) {
method SetZN (line 58) | public void SetZN(byte value) {
method Fetch (line 79) | private byte Fetch() {
method Fetch16Bits (line 83) | public ushort Fetch16Bits() {
method RequestIRQ (line 90) | public void RequestIRQ(bool line) {
method RequestNMI (line 94) | public void RequestNMI() {
method ExecuteInstruction (line 98) | public int ExecuteInstruction() {
method LDR (line 292) | private int LDR(ref byte r, Func<AddrResult> mode, int baseCycles) {
method STR (line 301) | private int STR(ref byte r, Func<AddrResult> mode, int baseCycles) {
method TRR (line 309) | private int TRR(ref byte r1, ref byte r2, Func<AddrResult> mode, int b...
method StackPush (line 317) | private void StackPush(byte value) {
method StackPop (line 323) | private byte StackPop() {
method TSX (line 329) | private int TSX(Func<AddrResult> mode, int baseCycles) {
method TXS (line 335) | private int TXS(Func<AddrResult> mode, int baseCycles) {
method PHA (line 340) | private int PHA(Func<AddrResult> mode, int baseCycles) {
method PHP (line 345) | private int PHP(Func<AddrResult> mode, int baseCycles) {
method PLA (line 350) | private int PLA(Func<AddrResult> mode, int baseCycles) {
method PLP (line 356) | private int PLP(Func<AddrResult> mode, int baseCycles) {
method AND (line 364) | private int AND(Func<AddrResult> mode, int baseCycles) {
method EOR (line 372) | private int EOR(Func<AddrResult> mode, int baseCycles) {
method ORA (line 380) | private int ORA(Func<AddrResult> mode, int baseCycles) {
method BIT (line 387) | private int BIT(Func<AddrResult> mode, int baseCycles) {
method ADC (line 399) | private int ADC(Func<AddrResult> mode, int baseCycles) {
method SBC (line 413) | private int SBC(Func<AddrResult> mode, int baseCycles) {
method CPR (line 428) | private int CPR(byte r, Func<AddrResult> mode, int baseCycles) {
method INC (line 441) | private int INC(Func<AddrResult> mode, int baseCycles) {
method DEC (line 450) | private int DEC(Func<AddrResult> mode, int baseCycles) {
method INR (line 459) | private int INR(ref byte r, Func<AddrResult> mode, int baseCycles) {
method DER (line 465) | private int DER(ref byte r, Func<AddrResult> mode, int baseCycles) {
method ASL (line 472) | private int ASL(Func<AddrResult> mode, int baseCycles) {
method LSR (line 489) | private int LSR(Func<AddrResult> mode, int baseCycles) {
method ROL (line 506) | private int ROL(Func<AddrResult> mode, int baseCycles) {
method ROR (line 524) | private int ROR(Func<AddrResult> mode, int baseCycles) {
method JMP (line 543) | private int JMP(Func<AddrResult> mode, int baseCycles) {
method JSR (line 549) | private int JSR() {
method RTS (line 564) | private int RTS() {
method BIF (line 572) | private int BIF(bool condition, Func<AddrResult> mode, int baseCycles) {
method FSC (line 585) | private int FSC(int bit, bool state, Func<AddrResult> mode, int baseCy...
method NOP (line 591) | private int NOP() {
method BRK (line 595) | private int BRK() {
method RTI (line 615) | private int RTI() {
method IRQ (line 627) | public int IRQ() {
method NMI (line 648) | public int NMI() {
type AddrResult (line 665) | private struct AddrResult {
method AddrResult (line 669) | public AddrResult(ushort addr, int extra) {
method Implied (line 675) | private AddrResult Implied() {
method Accumulator (line 679) | private AddrResult Accumulator() {
method Immediate (line 683) | private AddrResult Immediate() {
method ZeroPage (line 687) | private AddrResult ZeroPage() {
method ZeroPageX (line 692) | private AddrResult ZeroPageX() {
method ZeroPageY (line 698) | private AddrResult ZeroPageY() {
method Absolute (line 704) | private AddrResult Absolute() {
method AbsoluteX (line 709) | private AddrResult AbsoluteX() {
method AbsoluteY (line 716) | private AddrResult AbsoluteY() {
method IndirectX (line 723) | private AddrResult IndirectX() {
method IndirectY (line 730) | private AddrResult IndirectY() {
method Indirect (line 738) | private AddrResult Indirect() {
method Relative (line 746) | private AddrResult Relative() {
method HasPageCrossPenalty (line 753) | private bool HasPageCrossPenalty(ushort baseAddr, ushort effectiveAddr) {
FILE: src/Cartridge.cs
class Cartridge (line 1) | public class Cartridge {
method Cartridge (line 20) | public Cartridge(string romPath) {
method CPURead (line 86) | public byte CPURead(ushort address) {
method CPUWrite (line 90) | public void CPUWrite(ushort address, byte value) {
method PPURead (line 94) | public byte PPURead(ushort address) {
method PPUWrite (line 97) | public void PPUWrite(ushort address, byte value) {
method SetMirroring (line 101) | public void SetMirroring(Mirroring mode) {
type Mirroring (line 108) | public enum Mirroring {
FILE: src/GUI.cs
class GUI (line 7) | public class GUI {
method GUI (line 20) | public GUI() {
method Run (line 46) | public void Run() {
method MenuBar (line 77) | public void MenuBar() {
method ScaleWindow (line 133) | public void ScaleWindow() {
method AboutWindow (line 150) | public void AboutWindow() {
method ManualWindow (line 175) | public void ManualWindow() {
class FileDialog (line 196) | class FileDialog {
method FileDialog (line 222) | public FileDialog(string startDirectory, bool canCancel = true) {
method Show (line 227) | public bool Show(ref string resultFilePath) {
method Open (line 283) | public void Open() {
FILE: src/Helper.cs
class Helper (line 1) | public class Helper {
method Flags (line 15) | public static void Flags(string[] args) {
method ASCII_NES (line 108) | public static void ASCII_NES() {
FILE: src/Input.cs
class Input (line 3) | public class Input {
method UpdateController (line 7) | public void UpdateController() {
method Write4016 (line 19) | public void Write4016(byte value) {
method Read4016 (line 25) | public byte Read4016() {
FILE: src/NES.cs
class NES (line 1) | public class NES {
method NES (line 5) | public NES() {
method Run (line 14) | public void Run() {
FILE: src/PPU.cs
class PPU (line 3) | public class PPU {
method PPU (line 43) | public PPU(Bus bus) {
method Step (line 67) | public void Step(int elapsedCycles) {
method RenderScanline (line 115) | private void RenderScanline(int scanline) {
method RenderBackground (line 125) | public void RenderBackground(bool[] bgMask) {
method RenderSprite (line 171) | public void RenderSprite(bool[] bgMask) {
method WritePPURegister (line 241) | public void WritePPURegister(ushort address, byte value) {
method ReadPPURegister (line 293) | public byte ReadPPURegister(ushort address) {
method Read (line 325) | public byte Read(ushort address) {
method Write (line 342) | public void Write(ushort address, byte value) {
method MirrorVRAMAddress (line 357) | private ushort MirrorVRAMAddress(ushort address) {
method WriteOAMDMA (line 377) | public void WriteOAMDMA(byte page) {
method IncrementY (line 385) | private void IncrementY() {
method IncrementX (line 403) | private void IncrementX(ref ushort addr) {
method CopyXFromTToV (line 412) | private void CopyXFromTToV() {
method GetSpriteColor (line 416) | private Color GetSpriteColor(int colorIndex, int paletteIndex) {
method GetColorFromPalette (line 422) | private Color GetColorFromPalette(int colorIndex, int paletteIndex) {
method DrawFrame (line 433) | public void DrawFrame(int scale) {
FILE: src/Program.cs
class Program (line 1) | public class Program {
method Main (line 2) | public static void Main(string[] args) {
FILE: src/Test.cs
class JSONTest (line 3) | class JSONTest {
class ProcessorState (line 4) | public class ProcessorState {
class Test (line 14) | public class Test {
method JSONTest (line 24) | public JSONTest() {
method Run (line 29) | public void Run(string jsonPath) {
FILE: src/TestBus.cs
class TestBus (line 1) | public class TestBus : IBus {
method TestBus (line 4) | public TestBus() {
method Write (line 10) | public void Write(ushort address, byte value) {
method Read (line 13) | public byte Read(ushort address) {
FILE: src/TestRunner.cs
class TestRunner (line 1) | public class TestRunner {
method TestRunner (line 3) | public TestRunner() {
method Run (line 7) | public void Run(string test) {
FILE: src/interface/IBus.cs
type IBus (line 1) | public interface IBus {
method Write (line 2) | void Write(ushort address, byte value);
method Read (line 3) | byte Read(ushort address);
FILE: src/interface/IMapper.cs
type IMapper (line 1) | public interface IMapper {
method Reset (line 2) | void Reset();
method CPURead (line 4) | byte CPURead(ushort address);
method CPUWrite (line 5) | void CPUWrite(ushort address, byte value);
method PPURead (line 7) | byte PPURead(ushort address);
method PPUWrite (line 8) | void PPUWrite(ushort address, byte value);
FILE: src/mappers/Mapper0.cs
class Mapper0 (line 1) | public class Mapper0 : IMapper { //NROM
method Mapper0 (line 4) | public Mapper0(Cartridge cart) {
method Reset (line 8) | public void Reset() {
method CPURead (line 12) | public byte CPURead(ushort address) {
method CPUWrite (line 25) | public void CPUWrite(ushort address, byte value) {
method PPURead (line 31) | public byte PPURead(ushort address) {
method PPUWrite (line 43) | public void PPUWrite(ushort address, byte value) {
FILE: src/mappers/Mapper1.cs
class Mapper1 (line 1) | public class Mapper1 : IMapper { //MMC1 (Experimenal)
method Mapper1 (line 12) | public Mapper1(Cartridge cart) {
method Reset (line 17) | public void Reset() {
method CPURead (line 26) | public byte CPURead(ushort addr) {
method CPUWrite (line 39) | public void CPUWrite(ushort addr, byte val) {
method PPURead (line 82) | public byte PPURead(ushort addr) {
method PPUWrite (line 103) | public void PPUWrite(ushort addr, byte val) {
method ApplyMirroring (line 109) | private void ApplyMirroring() {
method ApplyBanks (line 118) | private void ApplyBanks() {
FILE: src/mappers/Mapper2.cs
class Mapper2 (line 1) | public class Mapper2 : IMapper { //UxROM (Experimental)
method Mapper2 (line 5) | public Mapper2(Cartridge cart) {
method Reset (line 10) | public void Reset() {
method CPURead (line 14) | public byte CPURead(ushort addr) {
method CPUWrite (line 26) | public void CPUWrite(ushort addr, byte val) {
method PPURead (line 32) | public byte PPURead(ushort addr) {
method PPUWrite (line 41) | public void PPUWrite(ushort addr, byte val) {
FILE: src/mappers/Mapper4.cs
class Mapper4 (line 1) | public class Mapper4 : IMapper { //MMC3 (Experimental)
method Mapper4 (line 20) | public Mapper4(Cartridge cart) {
method Reset (line 25) | public void Reset() {
method RunScanlineIRQ (line 46) | public void RunScanlineIRQ() {
method IRQPending (line 62) | public bool IRQPending() {
method ClearIRQ (line 66) | public void ClearIRQ() {
method CPURead (line 70) | public byte CPURead(ushort address) {
method CPUWrite (line 91) | public void CPUWrite(ushort address, byte value) {
method PPURead (line 138) | public byte PPURead(ushort address) {
method PPUWrite (line 153) | public void PPUWrite(ushort address, byte value) {
method ApplyBankMapping (line 161) | private void ApplyBankMapping() {
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (117K chars).
[
{
"path": ".gitignore",
"chars": 64,
"preview": ".vscode/\nobj/\nbin/\nbulid/\npublish/\n\nextra/\ntest/v1/\n\n*.bin\n*.nes"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "MIT License\n\nCopyright (c) 2025 Bot Randomness\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "NET-NES.csproj",
"chars": 830,
"preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>net6.0</Targe"
},
{
"path": "README.md",
"chars": 23400,
"preview": "<!-- PROJECT LOGO -->\n<h1 align=\"center\">\n <br>\n <a href=\"https://github.com/BotRandomness/NET-NES\"><img src=\"git-res/"
},
{
"path": "src/Bus.cs",
"chars": 1612,
"preview": "public class Bus : IBus{\n public CPU cpu;\n public PPU ppu;\n public Cartridge cartridge;\n\n public byte[] ram;"
},
{
"path": "src/CPU.cs",
"chars": 24115,
"preview": "public class CPU {\n public byte A, X, Y;\n public ushort PC, SP;\n public byte status; //Flags (P)\n\n private c"
},
{
"path": "src/Cartridge.cs",
"chars": 3123,
"preview": "public class Cartridge {\n public byte[] rom;\n\n public byte[] prgROM;\n public byte[] chrROM;\n\n public int prg"
},
{
"path": "src/GUI.cs",
"chars": 10178,
"preview": "#pragma warning disable\n\nusing Raylib_cs;\nusing rlImGui_cs;\nusing ImGuiNET;\n\npublic class GUI {\n private NES nes;\n\n "
},
{
"path": "src/Helper.cs",
"chars": 5664,
"preview": "public class Helper {\n public static int scale = 2;\n public static string romPath = \"\";\n public static bool deb"
},
{
"path": "src/Input.cs",
"chars": 1169,
"preview": "using Raylib_cs;\n\npublic class Input {\n private byte controllerState = 0;\n private byte controllerShift = 0;\n\n "
},
{
"path": "src/NES.cs",
"chars": 535,
"preview": "public class NES {\n Cartridge cartridge;\n Bus bus;\n\n public NES() {\n cartridge = new Cartridge(Helper.ro"
},
{
"path": "src/PPU.cs",
"chars": 16775,
"preview": "using Raylib_cs;\n\npublic class PPU {\n private Bus bus;\n\n private byte[] vram; //2KB VRAM\n private byte[] palett"
},
{
"path": "src/Program.cs",
"chars": 392,
"preview": "public class Program {\n public static void Main(string[] args) {\n Console.WriteLine(\"NET-NES\");\n\n Help"
},
{
"path": "src/Test.cs",
"chars": 4476,
"preview": "using Newtonsoft.Json;\n\nclass JSONTest {\n public class ProcessorState {\n public int pc { get; set; }\n p"
},
{
"path": "src/TestBus.cs",
"chars": 335,
"preview": "public class TestBus : IBus {\n public byte[] ram; //64 KB RAM\n\n public TestBus() {\n ram = new byte[65536];\n"
},
{
"path": "src/TestRunner.cs",
"chars": 3817,
"preview": "public class TestRunner {\n\n public TestRunner() {\n\n }\n\n public void Run(string test) {\n if (test != \"all"
},
{
"path": "src/interface/IBus.cs",
"chars": 100,
"preview": "public interface IBus {\n void Write(ushort address, byte value);\n byte Read(ushort address);\n}"
},
{
"path": "src/interface/IMapper.cs",
"chars": 214,
"preview": "public interface IMapper {\n void Reset();\n \n byte CPURead(ushort address);\n void CPUWrite(ushort address, by"
},
{
"path": "src/mappers/Mapper0.cs",
"chars": 1281,
"preview": "public class Mapper0 : IMapper { //NROM\n private Cartridge cartridge;\n\n public Mapper0(Cartridge cart) {\n c"
},
{
"path": "src/mappers/Mapper1.cs",
"chars": 5001,
"preview": "public class Mapper1 : IMapper { //MMC1 (Experimenal)\n private Cartridge cartridge;\n\n private byte shiftRegister ="
},
{
"path": "src/mappers/Mapper2.cs",
"chars": 1372,
"preview": "public class Mapper2 : IMapper { //UxROM (Experimental)\n private Cartridge cartridge;\n private byte prgBank;\n\n "
},
{
"path": "src/mappers/Mapper4.cs",
"chars": 6456,
"preview": "public class Mapper4 : IMapper { //MMC3 (Experimental)\n private Cartridge cartridge;\n\n private byte bankSelect;\n "
},
{
"path": "test/README.txt",
"chars": 138,
"preview": "JSON test goes here.\ntest/v1/XX.json\n\nJSON test repo used from SingleStepTest on GitHub for 6502: https://github.com/Sin"
}
]
About this extraction
This page contains the full source code of the BotRandomness/NET-NES GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (109.5 KB), approximately 30.5k tokens, and a symbol index with 171 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.