Running .NET on the NES | BRK252

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
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.
Info
Channel: Microsoft Developer
Views: 55,917
Rating: undefined out of 5
Keywords: Advanced (300), BRK252, Breakout, English (US), Game Development, Jon Peppers🌶️, Other, Running .NET on the NES | BRK252, Version v2, build, build 2024, microsoft, microsoft build, microsoft build 2024, ms build, ms build 2024, msft build, msft build 2024, n2f9
Id: ASTqqvQo0dM
Channel Id: undefined
Length: 42min 51sec (2571 seconds)
Published: Mon May 27 2024
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.