dotnes: writing NES games in C# with dotnet new
.NET for the NES game console
At a glance
- What is it?
- dotnes is a .NET library for the Nintendo Entertainment System that exposes the PPU, APU, and OAM as C# APIs, ships a dotnet new template, and compiles a project into a .nes ROM. Its stated goal is that a ROM built this way is byte for byte identical to the same program written in C, and every published release so far is a beta.
- Who is it for?
- dotnes fits a C# developer who wants to write a NES game with a modern language, who is comfortable with the constraint that the output is byte for byte comparable to a C build, and who wants the PPU, the APU, and metasprite handling exposed as ordinary C# calls. It does not fit someone who needs a finished game rather than a platform, and it does not fit a team unwilling to track a pre-1.0 API, since every published release is a beta.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 9 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A C# object model for the console's hardware
dotnes is described simply as .NET for the NES game console, and the point of the project is stated in its own scope section: an object model for writing NES binaries.
What that means concretely is that the console's hardware is available as C# API calls. The picture of the PPU, the APU, the OAM, the video RAM, the palettes, the controllers, and the memory map are all reachable from managed code, and the build produces a real .nes ROM file rather than something you run in a managed runtime.
That distinction is the whole trick. A NES program is a 6502 binary with a fixed memory map and a hardware coprocessor, so the compiler here is not compiling to IL, it is compiling to 6502 machine code and laying out a ROM. The output is a file an emulator or a console can run.
The project is written in C#, licensed under MIT, and organised as a normal .NET repository with a solution file, Directory.Build.props and Directory.Build.targets for shared build settings, a global.json pinning the SDK, and a samples directory.
The sample list is where the ambition shows. There are 46 sample projects, and the README shows them as a grid of animated GIFs. The names cover the ground you would expect from a console library and then some: shoot2 and pong and snake and climber as games, procgen for procedural generation, metasprites, animation, a scoreboard, bigsprites, sprites, horizscroll, vertscroll, scroll, fade, a slideshow, monobitmap, music, fami, conio, metacursor, metatrigger, horizmask, a statusbar, peekpoke, irq, an rletitle, an attributetable, a tileset, hello, tint, and flicker.
The presence of peekpoke, irq, metatrigger and conio tells you the library is not only a friendly layer over the graphics. It reaches down to the parts of the console that assembly programmers use.
Install the template, create a project, run it
The getting started path is three commands, and it is the dotnet template mechanism rather than a separate toolchain.
The template package is installed first.
dotnet new install dotnes.templatesThen a project is created from it.
dotnet new nesAnd the project is built and run like any console application.
dotnet runThere is also a project template for Visual Studio, so a developer who prefers an IDE can create the project from the template picker and hit F5 without touching the command line at all.
The README carries one small warning for the Visual Studio Code route, and it is the kind of thing that costs an afternoon if you do not know it. Ctrl+F5 currently works better in C# Dev Kit in VS Code, which is a note about debugging in the editor rather than about the library itself.
The build is configurable, and the configuration surface is documented separately. .NES exposes several MSBuild properties for configuring your ROM, including the mapper, mirroring, PRG and CHR bank counts, and diagnostic logging, with the full reference in docs/msbuild-properties.md. Mapper and mirroring are the two settings that decide how a multi-cartridge board behaves, and bank counts decide how much of the tile data and program data fit into the ROM, so these are not cosmetic options.
The GIF of the emulator launching from Visual Studio Code in the README is the shortest possible demonstration of the whole loop, and it is worth pausing on, because the speed of that loop is the main argument for the project.
The API shape: one Program.cs, two implicit usings
The anatomy of an NES application in this framework is short, and the Hello World example shows the whole shape of the programming model.
The program sets palette colours with pal_col, writing a colour index and a colour, then writes text to the name table with vram_adr, using the NTADR_A helper to compute the address for a tile row and column, then vram_write to put the bytes into video RAM. It enables PPU rendering with ppu_on_all, which turns the screen on, and then loops forever.
The C# idiom is deliberate. The README points out that this looks very much like the classic 8-bit workshop Hello World in C, taking advantage of the latest C# features in 2023, and the comparison is fair because the API is a direct transliteration of the 6502 originals.
What is different is how the API gets into scope. By default the APIs like pal_col are provided by an implicit global using static NESLib, and the colour constants like DarkBlue by a global using static NESColor. So there is no using block, and the namespace prefix disappears from the code entirely. And all the code is written within a single Program.cs, which is the opposite of how a modern C# project is usually arranged, and is right here, because on a NES the program is a single entry point on a single ROM.
There is an art asset as well, and the README is self aware about it. A chr_generic.s file is included as your game's artwork, and the file is an assembly-style data listing with a segment directive for the character data and rows of byte values, followed by the actual pattern bytes. That table of data is used to render sprites, text, and so on.
So a dotnes project is two files at its heart, one Program.cs and one .s of character data, plus whatever you add.
Music through the APU, in pulse and triangle channels
The audio support is not a wrapper around a sound file. The APU of the console is a synthesis chip with two pulse channels, a triangle channel, a noise channel, and a length counter, and dotnes exposes it as the APU primitives, which means music on a NES is written the way it was on a NES.
The music sample plays The Easy Winners by Scott Joplin using the pulse and triangle channels, and the code shows the three tables it needs.
The first is a note table, an array of numbers, which goes into set_music_pulse_table. The second is a triangle table, which goes into set_music_triangle_table. The third is the music data itself, a byte array passed to start_music.
The runtime loop is three calls. apu_init prepares the sound hardware, start_music begins playback, and then the loop waits for the next vertical blank with ppu_wait_nmi, which is the same synchronisation primitive the graphics code uses, and advances the playback with music_tick.
That synchronisation point is the interesting engineering decision. The music is not played on a background thread with a timer, it is advanced once per frame, on the same vertical blank interrupt that the rest of the program uses to keep the PPU and the CPU in step. That is why the code is correct on real hardware and not just in an emulator, and it is also why the arrays are plain ushort and byte arrays that you can write in C# and have the runtime place into the APU's memory.
The music sample is one of the 46, and the details live in docs/music-sample.md.
Metasprites, and the parts of the console that are not graphics
The metasprites sample is the one to read if you want to see what the library is really doing, because it combines several things at once.
It demonstrates runtime arrays, for loops, and random numbers, and it builds a metasprite, which is the technique of combining several hardware sprites into one larger sprite. The data is an array of tuples, one per hardware sprite, giving the tile index and the x and y offsets, with a terminator value at the end of the list.
The loop then walks sixteen actor positions, filling the x and y arrays with rand8, the random number call, and calls oam_meta_spr to write each actor's sprites into the OAM. After that, oam_hide_rest is called to hide the remaining sprites in the OAM that the metasprites did not use, and the frame is closed with ppu_wait_frame.
Two things in that sample are worth naming as a design point. The actor positions are runtime arrays, not compile time constants, so the actors move and the sprite data changes every frame, which is the difference between a static background and a game. And the sprite budget is handled explicitly, because the NES has a fixed number of sprites and the library makes you deal with the overflow rather than hiding it.
The other sample names point at the same level of access. peekpoke is the direct memory access routine, irq is interrupt handling, metatrigger is the sprite zero hit mechanism, conio is console I/O, horizmask is a graphics masking technique, and the bankswitch and mmc1 directories, visible in the repository layout, are mapper and memory banking work.
The native runtime integration document describes the other side of that: native assembly callbacks and opting into exclusive PPU and OAM ownership without depending on private runtime addresses, with callback registration, extern symbol compatibility, and interrupt responsibilities covered. The last point is the one that matters for anyone extending the library, because it means the native side has a supported interface rather than a set of hardcoded addresses.
Byte for byte identity, and a pre-1.0 API
The scope section of the README is the most interesting paragraph in it, because it states the acceptance criteria rather than the features.
The types of things the author wanted to get working initially are listed as an object model for writing NES binaries, a build that should produce a .nes binary that is byte for byte identical to a program written in C, a Hello World that runs, byte arrays and a more advanced sample like the attributetable one running, and local variables working in some form.
The byte for byte criterion is the strong one, and it is the kind of requirement that keeps a compiler honest. If the C# build emits exactly the same bytes as the equivalent C program, then the compiler, the runtime, and the memory layout all match the reference implementation, and every difference you see in a ROM is your code rather than a divergence in the toolchain. It also means the generated ROM can be compared against a known good binary, which is a test that either passes or does not.
The last criterion, local variables working in some form, is a good indicator of how early the project still is, and the release history confirms it. The three most recent releases are all betas, 0.3.2-beta in July 2026, 0.3.1-beta in May 2026, and 0.3.0-beta in March 2026, and they carry informal names, Just another patch, Moar APIs!, and the vibes are off the charts. The repository is not archived and the last push was on 2026-10-14, so the work is ongoing.
For anyone planning to build on this, the practical reading is that the API will move. A 0.x library with an explicit byte level compatibility goal is a research platform first and a stable dependency second.
One more capability is worth flagging for tooling authors. The in-memory compilation API lets tools that need a 6502 object model work without writing a ROM at all, and standard .NET test projects can reference the dotnes package directly to use that API without the ROM build settings or the NES-only analyzers. That is what makes it possible to unit test NES code in a normal test project, and it is a much bigger deal for a compiler project than it first appears.
Editorial conclusion
dotnes fits a C# developer who wants to write a NES game with a modern language, who is comfortable with the constraint that the output is byte for byte comparable to a C build, and who wants the PPU, the APU, and metasprite handling exposed as ordinary C# calls. It does not fit someone who needs a finished game rather than a platform, and it does not fit a team unwilling to track a pre-1.0 API, since every published release is a beta. Verify first that the template installs and that dotnet run produces a ROM you can load in an emulator, that you read docs/msbuild-properties.md for the mapper, mirroring and bank count settings, and that your tools use the in-memory compilation API rather than depending on the runtime addresses the native integration path deliberately avoids.
Frequently asked questions
How do I create a new NES project with dotnes?
Install the template package with dotnet new install dotnes.templates, create a project with dotnet new nes, and run it with dotnet run. There is also a project template for Visual Studio, so the project can also be created from the IDE and started with F5.
What does dotnes produce when it builds?
A .nes binary. The stated goal is that the ROM is byte for byte identical to a program written in C, which is the acceptance criterion the author set for the project alongside running Hello World and making local variables work.
How do I write music in a dotnes project?
Through the APU. The music sample plays The Easy Winners by Scott Joplin using the pulse and triangle channels, with a note table passed to set_music_pulse_table, a triangle table passed to set_music_triangle_table, and the music data itself passed to start_music, after apu_init, with playback advanced by music_tick on each ppu_wait_nmi.
Can I test dotnes code without building a ROM?
Yes. The in-memory compilation API gives tools that need a 6502 object model a way to work without writing a ROM, and standard .NET test projects can reference the dotnes package directly to use that API without the ROM build settings or the NES-only analyzers.
Is dotnes ready for production use?
The three most recent releases are all betas, 0.3.0-beta, 0.3.1-beta, and 0.3.2-beta, with the last one published in July 2026, so the API is still moving. The repository is not archived and was last pushed on 2026-10-14.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/jonathanpeppers-dotnes)Was this useful?