Page 4 of 4
Posted: Fri Jun 22, 2012 1:59 pm
by Shiru
It is still possible to make thousands of nice games with just a couple of moving objects on screen (in fact, it was up to 5 objects and 16 tiles in Alter Ego). You know, even the current best selling NES homebrew game does not have much more moving objects and tiles than in 'a C game', and it would be easy to make it in C.
Posted: Fri Jun 22, 2012 2:20 pm
by 3gengames
I didn't know sales determined how well a game was programmed. In that case, commando is better than all NES homebrews? lol.
Posted: Fri Jun 22, 2012 2:22 pm
by strat
ROM space is also an issue. Compiled C code will probably take a lot more space, and even if say Super Mario Bros. could be written in C, it wouldn't fit in 32kB.
Posted: Fri Jun 22, 2012 2:57 pm
by Shiru
It does not matter how well a game was programmed at all, as long as the game is good. I think it is very obvious that there is a reason why the game is a best seller, and the reason - the game is good. You can make a game like this in C.
I think I repeated it like a hundred times everywhere that the ROM space is the main issue with programming for the NES in C.
Posted: Fri Jun 22, 2012 4:04 pm
by rainwarrior
I've been experimenting with CC65 with AOROM. It was a bit of a pain to get going, but it's been working well so far. This helps a lot with the code space issue. Some things that I needed to do:
1. Compile and link each 32kb bank as a separate binary, and then merge the banks into your ROM manually. (copy /b header.bin+bank0.prg+bank1.prg... game.nes) I couldn't figure out a way to link in the required C runtime stuff to every bank otherwise.
2. Don't use DPCM for sound; this lets you switch banks more freely. (Or saves you from having to duplicate DPCM sounds in every bank at an inconvenient location in the middle of your bank.)
3. Divide banks by function, rather than having a full copy of all code in each bank. e.g. you can put all your music code and data in one bank, JSR to a routine that bankswithches, runs the music update, bankswitches back, and RTSes. Similarly, all the CHR data and code for loading CHR-RAM can go in one bank, etc.
I like AOROM especially, because for a repro it's only one extra (low cost) chip and capacitor vs NROM. Also if it was good enough for Battletoads, it's probably good enough for me.
Posted: Fri Jun 22, 2012 4:23 pm
by strat
Shiru wrote:
I think I repeated it like a hundred times everywhere that the ROM space is the main issue with programming for the NES in C.
Whoops, just noticed it in your first post.
Posted: Sat Jun 23, 2012 1:39 pm
by Bregalad
strat wrote:ROM space is also an issue. Compiled C code will probably take a lot more space, and even if say Super Mario Bros. could be written in C, it wouldn't fit in 32kB.
Again, only a good benchmark will tell us.
Usually optimizations optimize both code size and speed. Then only for the highest level optimizations, the compiler lets you choose between size or speed because optimize one will hurt the other. - Typically use a lot of LUTs will improve speed, but cost bytes.
When I said a compiler should perform at least 2/3 as good as assembly written code, I was counting the size as well as the speed.
Posted: Tue Jun 26, 2012 1:25 pm
by Jarhmander
Sorry for the bump. And it's a bit off topic too (well, out of the original question) but I thought of something and verified a believed misonception.
Bregalad wrote:[Using a software stack] is the very reason CC65 performs so poorly.
However, SDCC does not have a call stack, at the expanse of not allowing re-entrant functions (functions that calls themselves).
This is something that is typically never, ever used. Why waste so much performance for something that is never used ?
Here there is a misconception that seems to be shared by others, or maybe no one noticed that (myself I did, until I re-read his post):
Being re-entrant means much more than allowing recursion, it means being able to give always the same results given the same input. Such a routine must not rely on global/static variables that are not constant.
Let's consider a subroutine which uses fixed memory locations as parameters and/or temp space. Such a subroutine cannot be used on two different contexts (2 or more threads, or main/ISR) because the other context(s) could corrupt data used in the first context.
Imagine a small subroutine, named "A" here, used in both the main routine and an ISR. If "A" is running in the main context, then an IRQ/NMI fires in the middle of "A", "A" in ISR will execute correctly, however it clobbers the temp space used in "A" in main context, so when returning to the latter, "A" will use bogus data that has been corrupted by the ISR.
How to overcome this problem? HI-TECH C compiler for PIC10/12/16 don't allow using a C function in both the main context and an ISR, forcing you to define 2 identical functions, one used in main context and the other in the ISR. You can also save its temp/parameter space on the stack when entering an ISR, making this subroutine virtually re-entrant, but then you can possibly save that data uselessly, using stack space for nothing. Using a macro with adresses as parameters is like using different functions for different contexts, but wasting ROM space for each macro invocation. Data stacks elegantly solve this problem; that's why, on modern CPUs, some addressing modes often uses the SP as a base address for indirect access, using efficiently the "regular stack" as a data stack too. Those CPUs are better suited for C also.
Posted: Tue Jun 26, 2012 1:31 pm
by Shiru
Wonder who and why would use the same function in main loop and ISR in a real NES game, or, even better, who would use a C function in an ISR in a real NES game at all.
I mean, that was said above is correct in theory and in practice for many platforms, but for given platform and application I would say having overhead of reentrance for this reason is impractical - it is better to avoid such situation and save resources.
Posted: Wed Jun 27, 2012 12:55 am
by Bregalad
What you say is totally correct, HOWEVER as far I know CC65 doesn't even allow to write ISRs in C...
Posted: Wed Jun 27, 2012 6:55 am
by tokumaru
~J-@D!~ wrote:Let's consider a subroutine which uses fixed memory locations as parameters and/or temp space. Such a subroutine cannot be used on two different contexts (2 or more threads, or main/ISR) because the other context(s) could corrupt data used in the first context.
So let's just stop saying "recursion" and start saying "running in parallel". I personally never had the need to have two instances of the same function running in parallel when coding for the NES. Typically, the focus of the main thread and the focus of IRQs and NMIs are very different: the main thread performs complex calculations and buffers the results, IRQs and NMIs use the buffered data as quickly as possible, so they'd hardly need to make use of the same functions.
Posted: Wed Jun 27, 2012 10:09 am
by rainwarrior
Re-entrant is a good word, since this describes both recursion and multithreading, and also implies less direct recursion where you may go a few functions deep before reusing one.
As stated the 6502 does not have stack relative addressing like the x86, so it lacks the more efficient/convenient way it is accomplished on later processors. It can still do it quite functionally, just less efficiently.
You can certainly write an interrupt routine in CC65, though there are some potential issues (if you need the virtual stack you may have to create a separate one for the IRQ "thread"; probably easier to just avoid stack usage). This probably isn't an important ability, though, as the things you tend to want to do with an IRQ on the NES usually require precise timing and/or efficiency that using C will make difficult.
Thanks to the OAM DMA for sprites, an all C NMI handler should at least be capable of refreshing all your sprites and a couple of tiles without having to know too much about how it gets compiled.
For convenience, CC65 has a --static-locals compile option which will make all local variables in the translation unit static automatically (and thus all functions are not re-entrant). Personally, I prefer to manage my variables more explicitly, but it's a convenient feature if you need it.