jonathanpeppers / dotnes

.NET for the NES game console
MIT License
643 stars 18 forks source link
csharp dotnet emulation hacktoberfest nes

.NES ("dot" NES)

dot NES logo

.NET for the NES game console!

Gif of NES Emulator launching from VS Code

Contributing

PRs of any kind are welcome! If you have a question, feel free to:

Thanks!

Getting Started

Simply install the template:

dotnet new install dotnes.templates

Create a project:

dotnet new nes

Or use the project template in Visual Studio:

Screenshot of the NES project template in Visual Studio

Build and run it as you would a console app:

dotnet run

Of course, you can also just open the project in Visual Studio and hit F5.

Note that Ctrl+F5 currently works better in C# Dev Kit in VS Code.

Check out the video for a full demo:

Check out the video

Anatomy of an NES application

"Hello World" looks something like:

// set palette colors
pal_col(0, 0x02);   // set screen to dark blue
pal_col(1, 0x14);   // fuchsia
pal_col(2, 0x20);   // grey
pal_col(3, 0x30);   // white

// write text to name table
vram_adr(NTADR_A(2, 2));            // set address
vram_write("Hello, world!");         // write bytes to video RAM

// enable PPU rendering (turn on screen)
ppu_on_all();

// infinite loop
while (true) ;

This looks very much like "Hello World" in C, taking advantage of the latest C# features in 2023.

By default the APIs like pal_col, etc. are provided by an implicit global using static NESLib; and all code is written within a single Program.cs.

Additionally, a chr_generic.s file is included as your game's "artwork" (lol?):

.segment "CHARS"
.byte $00,$00,$00,$00,$00,$00,$00,$00
...
.byte $B4,$8C,$FC,$3C,$98,$C0,$00,$00
;;

This table of data is used to render sprites, text, etc.

Scope

The types of things I wanted to get working initially:

Down the road, I might think about support for:

How it works

For lack of a better word, .NES is a "transpiler" that takes MSIL and transforms it directly into a working 6502 microprocessor binary that can run in your favorite NES emulator. If you think about .NET's Just-In-Time (JIT) compiler or the various an Ahead-Of-Time (AOT) compilers, .NES is doing something similiar: taking MSIL and turning it into runnable machine code.

To understand further, let's look at the MSIL of a pal_col method call:

// pal_col((byte)0, (byte)2);
IL_0000: ldc.i4.0
IL_0001: ldc.i4.2
IL_0002: call void [neslib]NES.NESLib::pal_col(uint8, uint8)

In 6502 assembly, this would look something like:

A900          LDA #$00
20A285        JSR pusha
A902          LDA #$02
203E82        JSR _pal_col

You can see how one might envision using System.Reflection.Metadata to iterate over the contents of a .NET assembly and generate 6502 instructions -- that's how this whole idea was born!

Note that the method NESLib.pal_col() has no actual C# implementation. In fact! there is only a reference assembly even shipped in .NES:

> 7z l dotnes.0.2.0-alpha.nupkg
   Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2023-09-14 14:37:38 .....         8192         3169  ref\net8.0\neslib.dll

If you decompile neslib.dll, no code is inside:

// Warning! This assembly is marked as a 'reference assembly', which means that it only contains metadata and no executable code.
// neslib, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null
// NES.NESLib
public static void pal_col(byte index, byte color) => throw null;

When generating *.nes binaries, .NES simply does a lookup for pal_col to "jump" to the appropriate subroutine to call it.

.NES also emits the assembly instructions for the actual pal_col subroutine, a code snippet of the implementation:

/*
* 823E  8517            STA TEMP                      ; _pal_col
* 8240  209285          JSR popa                      
* 8243  291F            AND #$1F                      
* 8245  AA              TAX                           
* 8246  A517            LDA TEMP                      
* 8248  9DC001          STA $01C0,x                   
* 824B  E607            INC PAL_UPDATE                
* 824D  60              RTS
*/
Write(NESInstruction.STA_zpg, TEMP);
Write(NESInstruction.JSR, popa.GetAddressAfterMain(sizeOfMain));
Write(NESInstruction.AND, 0x1F);
Write(NESInstruction.TAX_impl);
Write(NESInstruction.LDA_zpg, TEMP);
Write(NESInstruction.STA_abs_X, PAL_BUF);
Write(NESInstruction.INC_zpg, PAL_UPDATE);
Write(NESInstruction.RTS_impl);

Limitations

This is a hobby project, so only around 5 C# programs are known to work. But to get an idea of what is not available:

What we do have is a way to express an NES program in a single Program.cs.

Links

To learn more about NES development, I found the following useful:

ANESE License

I needed a simple, small NES emulator to redistribute with .NES that runs on Mac and Windows. Special thanks to @daniel5151 and ANESE. This is the default NES emulator used in the dotnet.anese package, license here.