Making SNES ROMS with C# and Matthew Shapiro's DotnetSnes
synopsis
Matthew Shapiro, a staff video architect at GameChanger, has a hobby project that lets C# programs become Super Nintendo ROMs. A Reddit post put it on the Hacker News front page for a day and brought its first outside contributor. The pipeline translates .NET IL to C, then uses an existing SNES C toolchain. He sees it as a general route to running C# anywhere a C compiler exists, and he plans to target the PlayStation 1 next.
Origins. The project began on parental leave, optimizing a friend's retro game engine on Wilderness Labs Meadow hardware, which led to writing a small GPU in C for an ESP32. Wanting C# there instead, and noting that Nim compiles to C, he tried translating MSIL to C. Its first target was eBPF, kernel programs that cannot allocate memory, which matched what his transpiler could handle. A friend then suggested the SNES.
How it works. DNTC builds a dependency graph from a root function and converts the stack-based IL into C statements. Shapiro said working ahead of time showed him how hard the JIT team's optimizations are. There are no reference types yet because there is no garbage collector, which matters little on a console with no dynamic allocation. Attributes rename functions and bind to native C and assembly symbols. Plugins adapt the output to PVSnesLib's non-C99 C. Everything links into one binary, so native calls are free.
Shoulders. Shapiro credits Mono.Cecil, PVSnesLib and the older SDK it came from, and the Tiled map tool, which gives collision handling. Hanselman built the Mario sample in WSL in about 10 minutes. Shapiro said someone has run Breakout on real hardware.
- DotnetSnes compiles C# to SNES ROMs by transpiling .NET IL to C. Shapiro, a staff video architect at GameChanger, built DNTC, a transpiler that walks a dependency graph from a root function and turns the IL stack machine into C. Inspired by Nim compiling to C, the aim is to run C# anywhere a C compiler exists, without the .NET team building ahead-of-time compilation for every platform. The C output then goes through an existing SNES C toolchain to the console's 65816 CPU.
- Hardware without dynamic memory allocation suits the transpiler for now. It does not yet support reference types because there is no garbage collector, which he named as his next task. That fitted eBPF, which he said Windows is also moving toward after the CrowdStrike outage, and he has proof-of-concept C# eBPF programs running in the Linux kernel. It also fitted the SNES, which has no dynamic allocation either.
- Attributes let C# and native C interoperate at zero cost. Calls work in both directions. Attributes for custom function names and native functions and globals map C# onto the C SDK's expectations, such as a lowercase
mainand assembly labels at fixed memory locations. Because everything links into one binary, native calls carry no overhead, which Hanselman compared to P/Invoke. - The project stands on community libraries. It builds on Mono.Cecil for reading IL and on PVSnesLib, a C SDK for the SNES that grew from an older one. Because PVSnesLib targets non-C99-compliant C, the transpiler takes plugins to adjust its output. The SDK's support for Tiled maps gives collision detection, and the Mario sample is a near line-by-line port while the Breakout sample is more idiomatic C#.
- It runs easily today and has already run on real hardware. Hanselman reported building the Mario ROM in WSL in about 10 minutes, after updating submodules recursively and using SSH keys. Shapiro said someone ran the Breakout game on a real Super Nintendo from a flash cart. His next aim is a basic PlayStation 1 C# engine, after a Pikuma course.