.NET for the NES game console!
PRs of any kind are welcome! If you have a question, feel free to:
Thanks!
Simply install the template:
dotnet new install dotnes.templates
Create a project:
dotnet new nes
Or use the 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:
"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.
The types of things I wanted to get working initially:
- An object model for writing NES binaries
- Building a project should produce a
*.nes
binary, that is byte-for-byte identical to a program written in C. - "Hello World" runs
- Byte arrays, and a more advanced sample like
attributetable
run - Local variables work in some form
- Project template, MSBuild support, IDE support
Down the road, I might think about support for:
- Methods
- Structs
- Multiple files
- Some subset of useful BCL methods
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);
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:
- No runtime
- No BCL
- No objects or GC
- No debugger
- Strings are ASCII
What we do have is a way to express an NES program in a single Program.cs
.
To learn more about NES development, I found the following useful:
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.