Programming for the NES in C or C++
Moderator: Moderators
Depending on how capable the assembler is, advanced macros can sometimes be a great substitute for real C code without hurting performance as much.
However, since C is such a ubiquitous language, it automatically comes with many other benefits e.g. reusable code, large toolset with syntax highlighting, static analysis, etc
However, since C is such a ubiquitous language, it automatically comes with many other benefits e.g. reusable code, large toolset with syntax highlighting, static analysis, etc
The problem here is not knowledge, it's time and motivation. Lots of people here know the NES better than most of the professional developers from back in the day (we've been studying it for over 10 years, while they only focused on it for short periods), and we know everything there is to know about game concepts like scrolling, collision detection, and everything else necessary for a big-scale game.Hamtaro126 wrote:But in the very distant future, Maybe it is possible if we keep learning everything about 65xx ASM by then...
However, coding a big game like SMB3 demands a lot of time, something we don't have because of our real-life jobs and bills to pay. When we do have the time, we lack the motivation, because it's such a huge undertaking anyway and we are not offered any sort of compensation for it.
It seems creating SMB3 comparable games is not a pure ASM vs. C thing then. It's a motivation thing.
Also, I suspect that we all know why higher level languages exist. Why EDLIN gave way to EDIT. If more savvy programmers worked on better C libraries then the difference between C and ASM performance would decrease. As long as we think C *OR* ASM - high or low - they'll always be a self-justifying difference in output.
The best way to convince a beginner or higher level language programmer to convert to ASM is to recommend something that allows for ASM but retains its higher level language underpinnings for faster development. This is what Fred Quimby did with Batari BASIC for the Atari 2600. It's a gateway drug to assembly - easy results and easy integration of assembly when needed.
NESICIDE should offer the best chance for high level language programmers to start NES development. Especially ones who expect a development environment to just work out of the box.
Here's a promising C library but I'm not sure how it works with the cc65 included in NESICIDE:
http://kkfos.aspekt.fi/projects/nes/kne ... -for-cc65/
Also, I suspect that we all know why higher level languages exist. Why EDLIN gave way to EDIT. If more savvy programmers worked on better C libraries then the difference between C and ASM performance would decrease. As long as we think C *OR* ASM - high or low - they'll always be a self-justifying difference in output.
The best way to convince a beginner or higher level language programmer to convert to ASM is to recommend something that allows for ASM but retains its higher level language underpinnings for faster development. This is what Fred Quimby did with Batari BASIC for the Atari 2600. It's a gateway drug to assembly - easy results and easy integration of assembly when needed.
NESICIDE should offer the best chance for high level language programmers to start NES development. Especially ones who expect a development environment to just work out of the box.
Here's a promising C library but I'm not sure how it works with the cc65 included in NESICIDE:
http://kkfos.aspekt.fi/projects/nes/kne ... -for-cc65/
Well maybe it's a bit too early for me to talk about this but I currently have a project where I'm going to write an entire game in assembly, and port it entirely in C, and this could be a benchmark for compilers. Before anyone is calling me crazy I'm talking about a simple puzzle game here, not something like SMB3. Also I'm at about 50% of the development stage of the assembly version.
The C version will have to use almost everything the C language offers too (complex data types, routines with loads of arguments etc...). This could be used as a benchmark for C compilers for how well they perform.
The reason to do this for a whole game instead of just some piece of code is that it will give constructive results. If you just use a random piece of code as a benchmark, it will not be sinificative enough to tell if the downgrade of performance done by the comiler is handicapping for a game or not.
First I'd try with CC65, but then I'd like to either port SDCC to the 6502, or do an unofficial port of CC65 which will have better performance. I don't know if I'll be able to do it, in fact I'll probably have some trouble as I have no experience in writing compilers, and this is a complex subject.
Ideally I think a program written in C should at the very worst be 1/3 less efficient than it's assembly counterpart, which should be acceptable in most cases. When it's not acceptable, anyone can still continue to use assembly.
However I have quite some experience both in programming in C, and definitely a whole load of experience programming in 6502 assembly, so maybe I'm one of the few people that could handle such a project so I'll try.
So yes this seems ambitious, but for some reason I think this should be useful to the NESDev communality to be able to program in C without being afraid that the resulting compiled code will eventually suck.
The C version will have to use almost everything the C language offers too (complex data types, routines with loads of arguments etc...). This could be used as a benchmark for C compilers for how well they perform.
The reason to do this for a whole game instead of just some piece of code is that it will give constructive results. If you just use a random piece of code as a benchmark, it will not be sinificative enough to tell if the downgrade of performance done by the comiler is handicapping for a game or not.
First I'd try with CC65, but then I'd like to either port SDCC to the 6502, or do an unofficial port of CC65 which will have better performance. I don't know if I'll be able to do it, in fact I'll probably have some trouble as I have no experience in writing compilers, and this is a complex subject.
Ideally I think a program written in C should at the very worst be 1/3 less efficient than it's assembly counterpart, which should be acceptable in most cases. When it's not acceptable, anyone can still continue to use assembly.
However I have quite some experience both in programming in C, and definitely a whole load of experience programming in 6502 assembly, so maybe I'm one of the few people that could handle such a project so I'll try.
So yes this seems ambitious, but for some reason I think this should be useful to the NESDev communality to be able to program in C without being afraid that the resulting compiled code will eventually suck.
Useless, lumbering half-wits don't scare us.
I think this experiment would work better if you made the C version first, and then ported it to assembly. Since C is more restricted than assembly, the program will have to be structured in a certain way for it to work, and when porting it to assembly you can obey the same structure. If you start will all the freedom of assembly, the C version will probably have to be completely redesigned. i think a comparison would be more meaningful if both programs had the same underlying architecture, and only the actual tasks were performed in either C or assembly.Bregalad wrote:I currently have a project where I'm going to write an entire game in assembly, and port it entirely in C
I don't know maybe you're right.
I've already started the game in assembly - the idea to port it to C and make a benchmark out of it only came later.
I think I could port it in C without changing the entire structure. It's not like if I were using lots of tricks - even when I code in assembly I usually stuck to standard structure where functions calls themselves with some arguments, and it's extremely rare I have a function returning more than one value - if this happens I'll just use global variables or anyother alternative to port it directly in C without changing the structure too much.
The reason I don't start with C is so I don't feel like I'm doing the compiler's job when I translate into assembly, which could influence my in my style of assembly writing.
I've already started the game in assembly - the idea to port it to C and make a benchmark out of it only came later.
I think I could port it in C without changing the entire structure. It's not like if I were using lots of tricks - even when I code in assembly I usually stuck to standard structure where functions calls themselves with some arguments, and it's extremely rare I have a function returning more than one value - if this happens I'll just use global variables or anyother alternative to port it directly in C without changing the structure too much.
The reason I don't start with C is so I don't feel like I'm doing the compiler's job when I translate into assembly, which could influence my in my style of assembly writing.
Useless, lumbering half-wits don't scare us.
Games of SMB3 scale are very difficult for homebrew devs not because of programming (which is maybe a bit tricky, but doable), but because of the scale, amount and quality of the content - things that homebrew devs not very good at, because they mostly programmers. SMB3 wouldn't be that great without these components.
My library is kind of low level, it mostly just exposes the NES hardware registers to the C code in an easy to use way together with some support routines like controller reading. It's OK for quickly prototyping some stuff (I have written various tests using it myself, much faster than it would have taken to write them in assembly), but for game development right now it's better to use a higher level library, like the one provided by Shiru in his C examples.slobu wrote:Here's a promising C library but I'm not sure how it works with the cc65 included in NESICIDE:
http://kkfos.aspekt.fi/projects/nes/kne ... -for-cc65/
That said I have sometimes been thinking about expanding the library with more "high level" stuff, but that's probably not going to happen until I actually need that stuff somewhere.
Neat. Is this game completely written in C, without any self-written Assembly?Dwedit wrote:Zooming Secretary is written in C, and has source code available.
Sorry, but I won't believe that.Disch wrote:Much, much simpler than C.
By the way, does Assembly have functions and the ability to re-use code? Can I play around with one and the same program or is anything that I write fixed until I tediously change many parts in the source code?
One example: If I have a game character that is made of various sprites, like Mega Man, can I write a function in Assembly that moves all the sprite objects with the same call? Or do I have to program the movement of each sprite object individually? Like when I decide that the character shall not consist of two, but of three sprite objects. In C, I would just edit the drawing routine:
Code: Select all
void MoveCharacter(int x, int y)
{
MoveSprite(spr1, x, y);
MoveSprite(spr2, x, y);
// The character shall consist of three sprites now,
// so I add this:
MoveSprite(spr3, x, y);
}Code: Select all
MoveCharacter(30, 122);Is something like that possible with Assembly? Or do I have to call all the steps for movement for each sprite individually and if I add one sprite, I have to add 20 more commands in my code?
- rainwarrior
- Posts: 8062
- Joined: Sun Jan 22, 2012 12:03 pm
- Location: Canada
- Contact:
6502 has the instructions JSR (jump to subroutine) and RTS (return from subroutine). JSR pushes the current program counter and jumps to your subroutine; RTS does the reverse, returning to the next instruction.
A C function will use these when compiled. Store your arguments somewhere (in registers or somewhere in memory), JSR to the function's code, then "return" will store the return values somewhere and RTS.
So yes, the hardware has a concept of a subroutine.
A C function will use these when compiled. Store your arguments somewhere (in registers or somewhere in memory), JSR to the function's code, then "return" will store the return values somewhere and RTS.
So yes, the hardware has a concept of a subroutine.
I believe the source code is available, so you can check it out yourself.JimDaBim wrote:Neat. Is this game completely written in C, without any self-written Assembly?
The code is as reusable as you make it, like any other programming language.By the way, does Assembly have functions and the ability to re-use code?
Sure.One example: If I have a game character that is made of various sprites, like Mega Man, can I write a function in Assembly that moves all the sprite objects with the same call?
Newbies often do that, but that's because they're newbies. =)Or do I have to program the movement of each sprite object individually?
This is fine, although you'd usually avoid this much function calling in a system as weak as the NES, no matter if you are working in C or in ASM.Like when I decide that the character shall not consist of two, but of three sprite objects. In C, I would just edit the drawing routine:
Like I said, you typically wouldn't call a function for each individual sprite because that would be too slow. You'd probably have a loop, something like "from sprite X to sprite X+Y do this", in which case you'd just modify the range of the loop and it would cost you no extra commands at all if you wanted to handle more sprites.if I add one sprite, I have to add 20 more commands in my code?
Most beginner tutorials use assembly in a very specific way, with lots of hardcoded commands, but don't let that fool you - assembly is as versatile and as reusable as any other language. Maybe even more, because it isn't constrained to data types and logic blocks. You can even apply OOP concepts fairly easily.
Assembly language has subroutines, which do the same thing as functions in C. For example, functions in C compile to subroutines in assembly language. It also has jump tables, which can be turned into method calls.JimDaBim wrote:By the way, does Assembly have functions and the ability to re-use code?
Yes. What typically happens is that you have a subroutine that takes X, Y, and frame arguments, calculates where each of the sprites that make up a frame go in relationship to the given coordinate, and writes all the sprites to a display list. The display list lists all sprites that are considered visible during this frame, and it grows from 1 element to up to 64 over the course of each frame before it's copied to the video chip.One example: If I have a game character that is made of various sprites, like Mega Man, can I write a function in Assembly that moves all the sprite objects with the same call?
So you add the third sprite to the list of the sprites that make up each frame, and the subroutine reads the longer list.The character shall consist of three sprites now
Here are some problems that I see with compiling C for the NES:
1) 6502 has a concept of a call stack, but only partially. JSR/RTS only record the PC, they do not push/pop local variables or function parameters. The 256 bytes on the 6502 is probably not enough space to handle such a call stack, so the compiled code will have to simulate its own stack.
This means that local variables and function parameters are all going to have to be accessed indirectly. This poses several problems on the 6502:
-) LDA (n),Y is 5-6 cycles. LDA abs is 4 cycles. This means for memory accesses alone, you're already running 25-50% slower.
-) Y has to be reserved as the new stack pointer, which means you have one less general purpose register. Those quick nested loops you can write in raw assembly now become cumbersome because C will have to use zero page memory for additional loop counters instead of Y. INY = 2 cycles vs. INC zp = ?5? cycles (recalling these cycle counts from memory, feel free to double check).
-) Since Y is the new stack pointer, if you want to use Y for any other purpose you have to back it up and restore it. Since Y is required for most indirect accesses, that means any pointer dereferencing now entails a STY/LDY/LDY, in addition to the actual dereference itself. Note that the STY+LDY+LDY actually takes longer than LDA (n),Y, so that's over a 100% slowdown.
Granted these can be optimized away, but not completely. And a bunch of those little slowdowns add up real fast.
2) Common C practices would murder performance. Things like using 'int' for general variables, which every C programmer does. If you are adhering to C standards, an int has to be at least 16 bits. Can you imagine what kind of slowdown (and memory consumption!) you'd get in an NES program if every variable is 16 bits wide?
Really, for a C program to really be effective on the NES, it would have to be written with an understanding not only of how the NES architecture operates, but also how the compiler operates. You'd have to do things like use lots of globals, minimize the number of parameters passed to functions, etc, etc... to really get peak performance. Note that these are things you're generally not really supposed to do in C.
1) 6502 has a concept of a call stack, but only partially. JSR/RTS only record the PC, they do not push/pop local variables or function parameters. The 256 bytes on the 6502 is probably not enough space to handle such a call stack, so the compiled code will have to simulate its own stack.
This means that local variables and function parameters are all going to have to be accessed indirectly. This poses several problems on the 6502:
-) LDA (n),Y is 5-6 cycles. LDA abs is 4 cycles. This means for memory accesses alone, you're already running 25-50% slower.
-) Y has to be reserved as the new stack pointer, which means you have one less general purpose register. Those quick nested loops you can write in raw assembly now become cumbersome because C will have to use zero page memory for additional loop counters instead of Y. INY = 2 cycles vs. INC zp = ?5? cycles (recalling these cycle counts from memory, feel free to double check).
-) Since Y is the new stack pointer, if you want to use Y for any other purpose you have to back it up and restore it. Since Y is required for most indirect accesses, that means any pointer dereferencing now entails a STY/LDY/LDY, in addition to the actual dereference itself. Note that the STY+LDY+LDY actually takes longer than LDA (n),Y, so that's over a 100% slowdown.
Granted these can be optimized away, but not completely. And a bunch of those little slowdowns add up real fast.
2) Common C practices would murder performance. Things like using 'int' for general variables, which every C programmer does. If you are adhering to C standards, an int has to be at least 16 bits. Can you imagine what kind of slowdown (and memory consumption!) you'd get in an NES program if every variable is 16 bits wide?
Really, for a C program to really be effective on the NES, it would have to be written with an understanding not only of how the NES architecture operates, but also how the compiler operates. You'd have to do things like use lots of globals, minimize the number of parameters passed to functions, etc, etc... to really get peak performance. Note that these are things you're generally not really supposed to do in C.
- rainwarrior
- Posts: 8062
- Joined: Sun Jan 22, 2012 12:03 pm
- Location: Canada
- Contact:
It's not that hard to optimize around the virtual stack. If you keep your functions void() and manage your temporary/local variables on the ZP/BSS yourself, this takes care of most of it without much effort.
Recursion is a problem, but there are still solutions for that.
And yes of course you should learn good practices for the platform you target. The CC65 manual will tell you to use char instead of int unless you need it, etc. (Or read shiru's guide.) Also, there's not a lot of harm in writing it quickly and inefficiently at first and optimizing later when you actually have performance problems.
Recursion is a problem, but there are still solutions for that.
And yes of course you should learn good practices for the platform you target. The CC65 manual will tell you to use char instead of int unless you need it, etc. (Or read shiru's guide.) Also, there's not a lot of harm in writing it quickly and inefficiently at first and optimizing later when you actually have performance problems.
Last edited by rainwarrior on Thu Jun 21, 2012 3:43 pm, edited 1 time in total.