Is it ever common for a non-fixed PRG bank to make a call or use data from another non-fixed bank using a routine in the fixed bank to retrieve the data or make the call? I was thinking this could be a potential way to further reduce duplication of data across PRG banks.
At one point, all of my game data was stored in one PRG bank at a time: music, level, sprites, animations, and entity code. Now, I've got sprites, animations and entity code in one bank and level and music data in another bank. However, sometimes duplication is still necessary in order to support multiple game data sets (for lack of a better term) which may have some of the same sprites/animations/entities. So I've been thinking about ways to reduce that duplication. Thoughts/advice?
Of course a fixed bank can and does act as a
trampoline between code in one bank and code in another. But if you have code that interacts with numerous banks of data, it's probably best to put that code in the fixed bank if possible.
That's how my code works at present. All bankswitching is done from the fixed bank, and any code that runs within a non-fixed bank only accesses data already in the currently loaded fixed-bank. What I'm thinking about is an extreme case where perhaps you have almost all of your 16k PRG block filled with entity code, and it needs to access meta sprites or animations in another 16K PRG block that also could not fit into the fixed bank. Does this ever happen in practice or is this an extreme idea/case?
*edit* come to think of it, I actually do have a couple of special situations where this occurs. When an entity tests the map, it calls a routine in the fixed bank which switches out the non-fixed bank, does the map collision test, then restores the previous bank and returns to the entity which called it. I guess my real question is: how far have you seen this technique go in commercial games? Do you ever find functions which can marshall a call between non-fixed banks to any routine or retrieve any data? Or are they always specialized such as the case I described?
This is a valid thing to do, but I'd avoid doing it too much because the function call and the bankswitches add a lot of overhead.
I'm trying to do the following in order to avoid having a switchable bank using another switchable bank: The fixed bank contains the main game engine, because this is usually the one that needs frequent access to the other banks (usually for level data). To give more space to the main engine, code that doesn't need access to multiple banks (such as title screens, menus, PPU update routines, and so on) is put in a switchable bank.
I don't know if the 16KB of the fixed bank will be enough to hold the main engine when it's complete, but at least I do my best to keep unrelated stuff elsewhere.
That's a good idea, thanks. Do you anticipate being able to fit all of your entity code into the fixed bank along with your engine? Since this is my first NES game, I have no way of knowing how much space my entity code will ultimately occupy. I went with the trampoline scheme described above to account for the possibility that the code could become quite large. I'll have to look into trying what you suggested and move some code out of the fixed bank that doesn't need to be there, and see how much I have left over. I guess the bottom line is, there's no single "correct" way of dealing with these issues, since any given choice represents some kind of trade off.
I recently had to move a lot of my drawing routines into fixed ROM. So much handier now when adding new menus etc. Means I can stick the name tables in whichever bank I like and call the drawing routines with the relevant bank number. Probably common sense stuff but I'm learning all the time.
I wish you could bank-switch DPCM - it would make my life a lot easier
Gradualore wrote:
That's how my code works at present. All bankswitching is done from the fixed bank, and any code that runs within a non-fixed bank only accesses data already in the currently loaded fixed-bank. What I'm thinking about is an extreme case where perhaps you have almost all of your 16k PRG block filled with entity code
What is entity code? All Google can find are HTML character entities.
Quote:
how far have you seen this technique go in commercial games? Do you ever find functions which can marshall a call between non-fixed banks
Yes. I know of three such call gates in Apple II products alone:
- Apple IIe call gate bankswitches in an alternate ROM bank.
- Apple II ProDOS has the so-called "MLI" call gate that switches from the upper ROM (containing BASIC and the Monitor) to the RAM (containing ProDOS).
- The IIGS call gate switches to 65C816 native mode (which allows execution outside the first 64 KiB of memory) and looks up routines in a jump table. If you have a IIGS, try going into the Monitor and listing the code at $00/F89C.
neilbaldwin: You can bankswitch DPCM on some mappers. I can think of at least Crazy Climber, MMC1, MMC3, and MMC5.
tepples wrote:
Gradualore wrote:
That's how my code works at present. All bankswitching is done from the fixed bank, and any code that runs within a non-fixed bank only accesses data already in the currently loaded fixed-bank. What I'm thinking about is an extreme case where perhaps you have almost all of your 16k PRG block filled with entity code
What is entity code? All Google can find are HTML character entities.
I was under the impression entity was a common term used on this forum to describe in-game objects that have a lifespan and some kind of intelligence/update routine. googling "nesdev entities" or "nesdev entity" comes up with several posts by me, and a few others using this term. I think I first adopted it after some discussions with Banshaku a while back. I was originally just going to call them "enemies" since I didn't have any plans for a non enemy entity, but later on I felt the more generic term was better.
I've seen 'actor code' used elsewhere.
Consider this: Make each bank's code a more or less self-contained module with a defined API, as if you were making the module for other people's code to call. This should help you define the entry points used by your trampoline.
Well there is many way it could be done, but you'd want to go into this order, the first being the best and the last being the worst :
1) Code and data related to it in one 16k bank
2) If 1) cannot be done because you have too much data, place code in the fixed bank and data related to it in multiple 16k banks
3) If 2) cannot be done because the fixed bank is aldready full, have code in a switched bank, and data related to it that exceed the switched bank being acedded trough a "trampoline" routine in the fixed bank (will be very slow !)
4) If it lags by using 3) because it was too slow, have your code repeated in multiple banks (at the same adress) so that it can bankswitch the data related to it quickly without using a trampoline
Another way to solve the problem is to use 32k banks (your reset, NMI, IRQ and bankswitching routines will HAVE to be done like point 4), so you can have code and related data in a 32k bank, wich is twice as large. However, if you still overflow that limit, you'll have to deal with something similar to point 4) in all cases. However, it is much less likely you'll need that because the banks are bigger.
The Guardian Legend does this a lot. Their CHR data for example is spread across several banks, and for engines that aren't in the fixed bank (title screen, password screen, ending, etc) they have to do some bankswap voodoo to get their graphics.
They have it streamlined down into a single subroutine that can handle all cases. When they want to call a routine from another unfixed bank, they embed the address and bank of the target subroutine as DATA after the call to the fixed bank helper routine, like this:
Code:
JSR Crossbank_bridge ;this routine performs the switch
.byte $03 ;target bank
.word $8134-1 ;address of target subroutine (in RTS form)
lda whatever ;continue code in this bank upon return
cmp something
beq somewhere
The switching routine itself builds a bridge back to the original bank using the stack and then reads the target address:
Code:
Crossbank_bridge:
STA tempA ;save A and Y
STY tempY
PLA ;pull return address off stack and
STA ptr1 ;store in a pointer variable for 3-byte data read.
CLC ;we also add 3 bytes and push it back on the stack,
ADC #$03 ;building a bridge back to where we started
TAY ;we add 3 so we return to the point AFTER the embedded
PLA ;data bytes
STA ptr1+1
ADC #$00
PHA
TYA
PHA
LDA current_bank ;push the current bank on the stack too
PHA
JSR Crossbank_Go ;perform the jump. JSR here is important.
;when the crossbank routine returns, it will return
;here
STA tempA
STY tempY
PLA ;pull the origin bank
JMP bankswitch ;JMP is important here. when bankswitch returns
;it will return to the proper place in the original bank
Crossbank_Go:
LDY #$03
LDA (ptr1),Y ;read target address in other bank
PHA ;address will be in the form of address-1 b/c
DEY ;of the RTS trick
LDA (ptr1),Y
PHA
DEY
LDA (ptr1),Y ;read target bank
STA current_bank ;bank switch and return to the target bank
TAY
LDA bank_table,Y ;UNROM bankswitch
STA bank_table,Y ;now we are in the target bank
LDA tempA ;restore vars
LDY tempY
RTS ;RTS to target routine
Just an example of how one commercial game handles this.
neilbaldwin wrote:
I wish you could bank-switch DPCM - it would make my life a lot easier
You can with some specific mapper like the MMC3 and some under ones that I don't remember.
In games that use CHR-RAM there are usually many pages of tile data, and this is a good example of when it's OK to do these things, because the goal is to use a big amount of data at once, and probably isn't even done during gameplay, when CPU time is a very important resource. If a similar routine was used several times during gameplay to fetch small pieces of data, there would certainly be a significant impact on performance.
Banshaku wrote:
neilbaldwin wrote:
I wish you could bank-switch DPCM - it would make my life a lot easier
You can with some specific mapper like the MMC3 and some under ones that I don't remember.
UNROM is easy to mod, replace the 74HC32 with a 74HC08 (replacing OR gates with AND gates) and it makes UNROM keep $8000-$BFFF fixed and $C000+ is banked. That's iNES mapper #180, for Crazy Climber, and it's the type of bank setup I used in that 512kB-sized 'Chipography' NSF.
Memblers wrote:
UNROM is easy to mod, replace the 74HC32 with a 74HC08 (replacing OR gates with AND gates) and it makes UNROM keep $8000-$BFFF fixed and $C000+ is banked.
The only disadvantage would be having to repeat the interrupt vectors in all banks.
And not being able to bankswitch when the DMC is playing
You're better off with something MMC3 style where you can swap $C000 and $8000/$A000
MetalSlime wrote:
The Guardian Legend does this a lot. Their CHR data for example is spread across several banks, and for engines that aren't in the fixed bank (title screen, password screen, ending, etc) they have to do some bankswap voodoo to get their graphics.
They have it streamlined down into a single subroutine that can handle all cases. When they want to call a routine from another unfixed bank, they embed the address and bank of the target subroutine as DATA after the call to the fixed bank helper routine, like this:
[...]
Just an example of how one commercial game handles this.
This really remembers me how I did to handle 32k switching, I have tried something almost exactly like what you describe
here
Disch wrote:
And not being able to bankswitch when the DMC is playing
True. All bankswitching would be for DMC, which means that this setup would not be suitable for a game...
Bregalad wrote:
This really remembers me how I did to handle 32k switching
The last time I tried using 32KB bankswitching I had a lot of repeated code... Like, every bank that had level map data also had routines for decoding rows and columns from it and for testing collision detection, so that I could just select the bank and perform the task I wanted, as opposed to bankswitching a ridiculous amount of times just to read the data and perform the tasks in another bank.
This might be a ridiculous suggestion, but you could copy code to RAM and execute it there. This would probably only be useful if you had the extra 8k of SRAM so you could copy larger pieces to it. But if you have only the 2k to work with (actually 1.75k, pretty much), then chances are you could squeeze any code you'd copy into the fixed bank.
This is why I really like 8k bankswitching. You still have a fixed bank, but you can swap out more than one bank. This is really handy for something like my cutscene engine with large rendering code in one bank, and large screen definitions in another. This code is so large that it can't fit into a 16k fixed bank and leave space for other code, and it also is very crucial that everything within that code is executed at the highest speed possible. The code also accesses the data in that bank really often, and the data chunks are often around 256 or more bytes. Copying that to RAM would take a long time as well as a lot of space in RAM. So if I couldn't bankswitch more than one bank and I didn't want to copy data to RAM, I would have to waste A LOT of cycles jumping back and forth between a data bank and a code bank.
Celius wrote:
This might be a ridiculous suggestion, but you could copy code to RAM and execute it there. This would probably only be useful if you had the extra 8k of SRAM so you could copy larger pieces to it.
I think it's a good idea, I mean today the SRAM only costs about ~$1, and I'm sure one could easily spend more than that on a fancy mapper alone. And gameplay-wise you can generally
do more with the RAM than the mapper. With my Squeedo design I figured I could get away with 32kB PRG-ROM paging largely for that reason - there are 4 8kB pages of PRG-RAM that can be banked independently, which is more than enough room for code and variables.
The vectors don't really matter when you consider that a mapper init routine can fit in probably 8 bytes per bank (load, store, JMP), and that you can point the NMI and IRQ vectors to SRAM.
A drawback to 32 KiB banks is having to switch banks behind DMC's back unless the mapper can map RAM into $C000-$DFFF.