JONATHAN PEPPERS:
Am I on? Hello. I'm glad everyone decided to stay for the
last thing of the day. Hopefully, it's interesting. My name is John Peppers, and I'm an engineer on dotNet for Android
and and dotNet Maui. Today I'm talking about
a really weird thing, running dotNet on the NES, and that means
actually the Nintendo. Hopefully this will be
a fun talk, hopefully. I will have some assembly
code on the screen, so hopefully we
get through that, but let's get into it. This is what I imagine
seeing this ad as a kid, and this is the first
console I ever,owned or played or whatever and
it was amazing to me. You have this box and you can turn your living
room into an arcade, a and obviously, to
me, that's great. If we look at the
specs on this thing, it's not too powerful compared
to what we have today, like, an eight bit
processor, 1.78 megahertz. It can only do 52 colors
and only 25 at once. Obviously the screen
resolution and how much memory the thing
has isn't very high as well. But so I'm from Kentucky. This is a dramatization of me earning money to buy
one of these things. I remember mowing my
grandfather's yard and earning up to $100, and then I had enough
to buy this thing. By the time I had $100, the Supernintendo came out and I wasn't going to save $300, but I just went with it. The games are great. I mean, hopefully, if you're here, you know what I'm talking about. Of course, the original
Mario Brothers classic best thing
to start with. Then the major upgrade, Mario three, I would say
is the next great one, Mario can now go
backwards, which is great. Then, of course, Duck hunt. This is one of those pictures that when I see the picture, I can hear the sound. I can hear that dog
ridiculing me for missing all the ducks. Let's
get into this. First of all, why
would someone do this? We'll talk about that. I'll do a demo of what I have working. We'll talk a bit about
the design I came up with and developer experience
of what it looks like. I'll show you hello world
and C and C Sharp and explain what a NES
game looks like when you build one and run one. We'll talk about the binary
format that is in a NES ram, which you can load an emulator, or if you have the
right equipment, you can flash it to
actual NES cartridge and put it in real hardware. Then I'll talk about how
the whole thing works, converting Microsoft
intermediate language to actual 6502 assembly. Why would I want to do this? Maybe I have weird hobbies. Maybe that's the main reason. But for me, I wanted to learn a little bit about
computing history. I had maybe one college
course on assembly, and my daily work, it seems useful to learn assembly code a little bit to just understand how
the whole thing works. Of course, this is already what I work
on, but doing this, you would learn
quite a bit about dotNet things like MSBuild, the build engine,
not the conference, how to make templates? Things like that. Also I got in pretty deep learning about MSIL and ahead of time compiler, so we'll talk about, what
that means in a bit. The obvious reason
to do it is because it's a huge nerd
flex to do this. I should have listed
that one first, maybe. Another perspective on
how small NES games are. There are only five
12 KB that can fit on this cartridge and I just picked a random
stat like the top, IOS apps on the App store. They're somewhere
150-200 megabytes. Even the app icon of a general IOS app would
fill up this cartridge, so not even the code. Just the app icon would
fill up this thing. Right. Let's demo and I'll show you what I
have working here. If this looks okay. Here I am in Visual
Studio looks all right. This is the new project
template dialogue thing. I'm going to go into
all project types. I'm going to pick NES. Do you think there's
anything in? I'm going to click on this.
Let's see what it does. Let's call it HelloBuildy. I'm doing this in visual studio, but you could be at
the command line. You could be in VS Code. You could dotNet new NES. The only thing I've done to set this up is install
the project template. But I'll open my program CS, which I'll explain
this in a bit, but let's just go ahead
and run it and let's see what happens here. It's building the project. I basically just hit the play button and
this is an emulator. I didn't make this emulator, but this is one called ANESE, which I think it stands
for another NES emulator. The author was happy for me
to use it for this project, and so, this is what's
working so far. Prove that it's real. It's not like weird
smoke and mirrors. Let's change a color, so that background color was blue, and like I was saying, it can only do a certain
number of colors. It's not RGB, but I happen
to know that the number five is a nice purple dotNet
color, so let's try that. Then let's just say hello, Microsoft build, it worked. That's a pretty good build time. A lot different than I'm used to doing Android and IOS things. Anyway, so let's look a
little bit about the files, so I showed Program CS. The other thing in here
is you've got to have at least one set of image data, and that's what this file is. It's so short that they type
out the bytes, apparently. But I have a viewer that we can look at
what this looks like, instead of weird letters. The project is just a
regular dotNet project, and all the magic happens inside of a
single new gate package, so we're able to just transform a dotNet project into an NES project with this
one line, basically. Then the emulator, I put that in a separate
package in case I make another emulator work one day that would give you
some choice of which one. I could show all files. Maybe we can zoom
in just to show a few things inside here. My project was hellobuildy, can we see that? There
we go. That looks right. The DLL file is what you would get from just building
a class library, and the NES file is the
actual ram that you could take and load it in any
emulator or whatever. That's pretty cool. Let's
talk about how I did that. I can get back to my slides. From current slide. This is a part it could get
a little hairy. I might have to put assembly on the screen and talk
about some things. But if you need to catch, a little shut eye before
the big celebration, this might be a good time. How did I even start to
think about doing this? I think Step 1 for
me was to focus on the bare bones of what I
could do to get it to work. When I hit "Play"
in Visual Studio, there was not a debugger. You can't put a break point, so ignore that part. There's no GC. The whole dotnet-based class libraries, those don't work. You can't write classes
or even methods. But this was really just, can this work at all is
where I was starting from. Because this is for fun, this thing is not a product. To be clear, I'm
not selling this. It doesn't have to be compatible with anything, basically. I focused really on the
hello world at first. A lot of times, something like this is when you get
hello world working, that's what I needed to really get things going and
get motivated to do it. Of course, that
workflow I showed, dotnet build, dotnet run, that was really the part
that I wanted to start with. How does this work? You've got the C# compiler, and we've got some code. I'll talk about what the
code does here in a bit. This part remains unchanged. Roslyn builds this library like it would any other library. I've turned off a setting that doesn't allow the
standard libraries, so we at least have
that going for us. Then this is a view of ILSpy, which is a great tool. That's what I use to debug what happened when we built this project,
what's the output? You can view C# code or IL, and this is a picture of IL. Then the next step is to
write an MSBuild task that can convert this IL into
what the NES understands. This is a screenshot
of 8bitworkshop.com. This process sounds
a little familiar. If you know about.NET, we have the just
in time compiler. Even since.NET
framework like 1.0, it loads MSIL at runtime as each method is
called on the first time, and it does this process. It takes IL and turns it into a
machine code that can run on the computer
that it's running on. Another thing I'm familiar with, so Mono has an AOT compiler that does this step
at build time. This is basically exactly what we are doing here for the NES. What's interesting
about the AOT compiler, we used it in Xamarin, we still use it.NET
8 and.NET MAUI. On Apple platforms like
iOS or Mac Catalyst, it's not actually
possible to run a JIT, and so this is a requirement
on those platforms. Apple documentation might say something like this is
a security feature, but it also gives
faster startup, which is good in general
for mobile apps. Lastly, we have a new AOT
compiler called Native AOT. This came out in.NET 7 for console apps which is
already pretty useful. If you're writing a little tool, I'd recommend looking into this like some
command line tool. In.NET 8, they added
even more support. I think they added some
ASP.NET experience, but we also have
experimental support for iOS and.NET 8 for Native AOT. We're going to have more
coming in.NET 9 in the future. 8bitworkshop, I'm not
affiliated with them. I'm just a fan. But this is the
tool I use to even figure out how to
do this at all. It has not only the NES, but there's a drop down
that has all kinds of old machines and emulators that you can write programs
for in the browser. You can write C code
in the middle here, and then there's an emulator that just runs live and you can actually put breakpoints and see the disassembly and
see what's going on. This is invaluable in figuring out how to
get this to work. Once you've got a
project working, you can download the ROM, just like I created
in the other project. Let's go through Hello World. This is what the code
looks like in C, so you have a void main. This is really the minimum
to get something working. The first method, pal_col
is palette colors, and so that just sets up the default colors
on the NES game. Then we have a VRAM address, in a macro called
NT adress_A, well, which the names were a little
abbreviated back then. But what this is doing
is moving the position. Maybe you can think
of it as the cursor. We're moving over two
pixels and down two pixels, and then we're drawing
the word hello world. We also have to parse in
the length of hello world, so that's what the 13 is. Then ppu_on_all
turns on the screen. Then lastly, you have to have an infinite loop for the
game to stay running. In a real game,
you would actually have code inside the WOW loop. But for a demo, you could just WOW through
basically forever. What would happen is
if that loop exited, that's what it
would be like when you've seen an NES game crash. My memory is a weird noise, and then the whole
screen is frozen. I think that's basically
the behavior that you would get if you
exited out of that loop, which would only
happen on some fault. What could we do in C#. One of my goals here was to
keep it looking the same so that if you're looking
at a tutorial in C, you could do the
same thing in C#, with the idea that one day, maybe I could come up with some API to make this look even prettier than
what it is today. We could use top
level statements. The latest C#, you don't have to have a static void main, so let's just use
the one program.cs. Those methods up there, that's actually a static
method on a class, and I'm using implicit
global usings to just make this look
like the C program, and of course, to use static to be able to automatically
call these static methods. Now, let's talk about my thought process in trying to simplify this even further. The code I showed there is
from a library called neslib, which is, you could
use that to write games or you could write the
games in pure assembly code, if you'd rather do that. But neslib, where I started is just take the header file
and paste it into a C# file, and just make that
valid C# and get a reference assembly
of that API surface, so that when you go
to build the project, it doesn't have to actually have method bodies for that code. It can just be a
reference assembly and show which code it
would have been called if it was a
real.NET program. Of course, we need
a project template. We're going to have the
MSBuild tasks and targets, which I'll talk
about a little bit. Then we get into the details of what is the NES
binary format, and how do we write that in C#? I also wanted to at first understand just
enough to be able to change the text on that one
instruction of hello world, because in my mind, if I was
able to change that text, that would motivate me
to finish these things. Then I'll use a library called
System.Reflection.Metadata, which is part of.NET. This is also similar
to Mono.CSL, if you've ever looked into something like that,
opening.NET assemblies. You can use this to iterate over IL and do
something with it. In this case, we're going to
convert it into assembly. I also wanted to convert
multiple samples. I have a few working. Then eventually the goal is
to write Super Mario and C#. I don't know if we'll get there
today, but maybe someday. Let's talk about the file format. At the very top of NES
ROM, there's a header, that's 16 bytes and
then there's a trainer, which I'll talk about
in just a sec here. The PRG ROM which I
assume stands for program ROM is where all your code lives and so
you can have a few chunks. Most of the samples I
have have two of these, 16 KB segments, and then a single CHR ROM which
I might call it a car ROM. I don't know if
that's character ROM. Sounds good. But games can have more than
one of these as well. If you're Mario and you go
to the underwater level, they're probably
swapping from one of these sprite sheets
to the other to get those underwater sprites, because it does
not look like that you could fit the
whole game onto one. The trainer. The trainer
is very interesting. Developers back in the day when they're working
on an NES game, let's say that you're working on Bowser's Castle boss fight, do you want to play the whole
game to get to that level? Not really. The point of the
trainer was so that they could put feature switches into the game
during development. Like, they might have
Mario be invincible. He has the star
at the beginning, or maybe you just boot the
game and it goes directly to Level 8 or whatever. That's what the
trainer was used for. One interesting product
is the Game Genie, which took advantage
of the trainer. The trainer is not
physically on a card. If you have a copy of
Super Mario Brothers, there's no trainer in it. But what the Game Genie
did is you would attach it onto the cartridge and put that in your NES
and they reverse engineered all of these feature switches
in all the games. The Game Genie had a menu
where you could pick, oh, I want to be invincible, and you would turn it on, and then it would boot the game. It would launch the trainer, and you're playing the game
in developer mode, basically. That's kind of a fun story. If you pop open a cartridge, one of those gray
NES cartridges, this is the board inside, and it has those
same letters, CHR, PRG, and that's all
the space you have. Your game has to fit in those
actual chips right there. There is an ability to
add expansion slots, but we're talking like, maybe we can add another
128 KB, not very much. The header, what it is, is literally the letters NES, followed by end of
file character, and then a couple
of bytes that say how many program ROM and character ROM segments
are on the cartridge. The flags also I think maybe byte six if there is
a trainer or not, and so that would be
something that Game Genie would have toggled to make
this whole thing work. The rest are zeros
for some reason. If you wanted to
write an NES header, so the writer here is just
a system IO binary writer, and you can just write
bytes as you would expect. You write NES, the end
of file character, the number of segments of
the two pieces of data, and then the rest zeros. When I first got
this working I had a unit test that
called this code, compared what I wrote against a real ROM and when
it actually worked, that was kind of a
light bulb moment. Let's look at the character ROM. These are two different
programs of the same image. There are a lot of
different editor tools for looking at this. The thing that's interesting
is that colors of the image are not
they're dynamic. Each tool might choose to
display them a bit differently. One of them has red letters
and the other one in black because when
you draw these, you content the colors, and that's how they
were able to make games with much fewer actual artwork. If you've played Mario 1, the clouds are white, and you turn them
upside down and they're green and now they're bushes. All kinds of tricks like
that are in the old games. This is just the
default sample sprite that it was on eight
bit workshop.com. It has a font, basically so any ask character is the first part of this image. Then the bottom is what you
have left to use in a level. That's pretty interesting. Let's talk about the
instruction set just briefly. Don't worry about reading this, but this is a table of every instruction
that the NES can do. The thing to note is there's
actually not that many. It was kind of a simple machine. Some other older computers
have this same instruction set like the Apple 2,
some commodore machines. I don't think the Commodore 64, but like some other
commodore machines use the same instruction set. It's kind of a well known, 6502 assembly is a
thing that people have known in history. If you look at the
instruction set that's in MSIL.net assemblies, it's actually a much larger
table than what we see here. This is going to be
a little pseudocode, but this is what it would take to fake our way and
make this work. This is my thought process in
the middle of this project. Now imagine my writer is not a binary writer
but an NES writer, and I have subclass
binary writer. We know how to write the header. That part works. Then I'm just going to ignore
a bunch of bytes. What I did is I took
a slice of a ROM that works and just saved it in my own file to
think about later. Then I figured out how to
write a single method call, which is that VRAM write
call with the text hello. Another segment of bytes
that I don't know. We'll call that segment 1. Then a string table. The VRAM write call, it doesn't have
the string there. What it has is an
address that can look up the actual string
data in a table that's at the bottom of the ROM. Then, of course, there's
one last section of unknown bytes. But this is my starting point. Once I saw this work, I was ready to keep going. Let's look at the.net side. This is a screenshot of ILS 5, which is still my favorite tool for looking at assemblies. You can look at all
the C# code inside. There's a tree view. You can open the name spaces
and see what's inside. The code that you'll see
in there is backed by these tables that are
under the metadata folder. For example, there's
a user string heap which is what we're
mostly interested in, and that'll contain any
strings that are in your code. Any just constant string
like hello world that's in the middle of your C#
code will be in this table. There's also a string heap. What that will
contain are actually like class names, method names. The string heap is more
about actual C# names of members while the user string is more like in line strings
that are within your code. What it looked like to load IL. This is using system
reflection metadata, and it is kind of a modern
version of model at CCL. One of the things that's
really good at is being fast and not allocating
unless it has to. On the second line here, the VR handle is
actually a struct. That's why the wild
loop looks a bit weird. Checking handle is Nil, a struck can't be null, and so they have a property to check if
the struct is empty. While you're looping
through the strings, it doesn't actually
allocate until you call get user string, and you give it a handle
to a row in the table, and you get the
actual string value. If we call the rider and
we pass this string in, you can imagine what that right string method
does is it just calls encoding ask bytes and just writes those bytes
to the underlying string. Then there's a zero byte
between each string. That part's kind of
straightforward. What about the
actual method call. This is what the code
in IL spy looks like, but there's a C#
view and IL view, and the numbers on the left are just like the offset
within the IL. You could probably ignore that. But then there'll be an
instruction in this case, load string, which based
on the instruction, it may or may not
have an operand. In this is case, it has a
single string value, hello. Then the second instruction. One method call to
us and C Sharp, we would imagine that that's just one thing,
it's actually two. For each argument
in a method call, it puts them onto a stack, and then actually
calls the method using the parameters
that are on the stack. Then the call operation
calls the actual method. This method is actually
in another table. Even in IL, everything
is based on tables. We just looked at the IL. This is what it would look
like on the NES side. For me, even now, I don't really know
6502 assembly. All I did is write unit tests, and do I get the same output? That's a good way
to go, I think. The first the blue
letters on the left, those are the actual bytes. The number A9, if you open NES ROM and like
a hex editor or something, and you went and
found this byte, the letter A9 means the instruction LDA,
which, who knows? I don't know what LDA is. Then F1 is the actual operand
for that instruction. One thing that's interesting is the NES is only eight bits. So what do you do if you have a number that's bigger than 255? In this example here, we see what they
do is they have to put part of the number
onto a register. The address for the string
table is bigger than 255, and so it calls a
subroutine named pushax in order to
make that math happen. JSR is Jump Subroutine
and Return, I believe. You can see where if
we go back to the IL, it's reasonable that we
could generate this code. Here's what it would
look like to do that. On my NES writer, I've created an enum
for every instruction, and then you can pass in the operator if
it's a single byte, and that makes it to
the underlying stream. Some of these here, for example, the call to pushax, it's bigger than one byte, and one thing that's
interesting is you can see that the numbers
are flipped around. The reason for that
is the NES was a little-endian machine where the smaller parts of
a number are first, which is a bit odd. I think that's backwards from
Windows and maybe some of the other operating systems
maybe we're used to. This looks
straightforward. At least, writing these bytes out. The next step for
me was to again, get all this code working, unit test, check that I
can emit a ram that's byte for byte identical
to one that was written with other tools. I got that to work. Let's
talk a bit about MSBuild. Does anyone here know
anything about MSBuild? It's like defense against the dark arts
for.NET developers. I love hate thing. If you have the
skills to use it, at least to read a log, that really saves the
day when your build is broken on a random day. This is actually the stuff
I work on in my day job, but MSBuild has two concepts. There's targets that
are written in XML, and then there are tasks
that are written in C Sharp generally. A target. I have a target here
named Transpile. I'm not a huge fan of using that word, but
that's what I put. It runs after the Build, so the regular.NET
Build is done. It has an input of Targetpath. Anything in here
with a dollar sign is how you reference
an MSBuild property. The input would be, for example, hello DLL, and the output
would be hello.nes. The way targets work
is if the input is newer than the output, or the output doesn't exist, it will run the target, and otherwise,
it'll get skipped. This is just general
good practice for writing MSBuild targets. Then I have a task here, Transpile the NES that this actually is C Sharp code that does the work
to make this happen. I pass in the files
that are needed. The @NESAssembly, that's
actually an item group, which is different
than a property, but an item group you
can think of as a list. You can have more
than one.s file and this would still work,
theoretically anyway. How do you launch
an NES emulator? This is actually even simpler
than writing a target, but it's just setting
a few properties. dotnet run, it
understands if you set the property run command
to something and run arguments and the
working directory, and when you call dotnet run, it'll evaluate your
project and just call these commands just
as you fill them out. That's how I'm able to
launch an emulator and do custom things
during dotnet run. Visual Studio understands
the same thing. You don't actually have to do any extra work to
make this happen. This might be useful if you
have other types of projects. Let's say you had a project
that runs some scripts, you could use the same
idea for that as well. The next step, I have these pieces of bytes
saved in files at this point. I want to get rid of those, because to me, that's not
really doing the real thing. I want to write the
whole NES binary and that is where I consider, oh, it really works
at that point. That first set of
bytes is actually all of the implementation
of neslib. We could take one of those methods that are in
the hello world example, palette color, pal call, and this is the
actual implementation of this sub routine. Just in the same way that we wrote the other bytes before, we can use the exact
same rider class to write out these bytes. This is pretty straightforward. Some instructions like TAX
here doesn't have an operand. This is at the
point where I would discover how some other
instructions actually work, was when I got to this far. There are only about 44 methods. It was a bit tedious, but this is something I
was doing spare time. Let's talk about
where it is today. What I showed you today, how much of it really works? This is a second sample. What this is showing is all the things that
you could do with a single piece of the
Character ROM on the image. This is a single little
rhombus, whatever. You can tent various colors, and this is just an example
showing how to do that. This sample had some other
interesting things in it. It had some in line byte
arrays that set up this data. There was some work to
get this one working. A couple of samples
are working today. I'm generating the
entire NES binary, and I don't have any pre stored sections
of bytes anymore. I'm pretty sure there's
one to do comment. There are four bytes that
I never figured out. It says to do. I have no idea what
these bytes are. It does match what
real ROMs have in it, so it does work. At the point I have this today, it's probably not going to
work to build a real game yet. That's something I can
keep working on it, maybe something in the future. But for now, if you wanted to learn more about
NES development, I would recommend just using 8bitworkshop and
learning a bit of C. There's other
development tooling out there that also can do
things in a graphical way. Some things that I
would like to do , more complicated branching, like a switch statement or some strange
combinations of IF Ls. Maybe those wouldn't work today. I'd also like methods to
automatically be sub routines. That would be
another great thing. It also would be possible
for struts to work, would be the next
thing I would look at. But one day, maybe we can get Super Mario
Bros in C Sharp. We actually may
not be able to do this because the game
is not open source. It's also one of
the first games, and so it was actually
written in pure assembly. That might be a little tricky. But what I thought
that does exist, there is likely a open source Mario-like game that we could probably get
working one day. If you want to try it
out, the project is on my GitHub,
github.com/jonathanpeppers. You'll probably find it. If you just want
to play with it, you can actually just
type dotnet new Install, and then the name of
the template packs, which is dotnes.templates. I called the project dotness, I don't know about the name. Maybe that's okay. I don't know. I have an AI generated logo
that's also not great, but maybe we could
work on that as well. But thanks. That's all I have. I
hope this was enjoyable. (applause) I hope you have
a good rest of the build. I'll be around for a bit
if you want to come chat or you want to pump me up and get me to
finish this thing. I'm happy to talk
about that. Thank you.