How to bank the code?

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
User avatar
pubby
Posts: 583
Joined: Thu Mar 31, 2016 11:15 am

Re: How to bank the code?

Post by pubby »

Most common technique is to use trampoline. A trampoline is a small piece of code that exists in every bank. It's called a trampoline because you jump in and out of it when changing banks.

A general purpose trampoline would look like this:

Code: Select all

.segment "FIXED_BANK"
trampoline:
  lda CURRENT_BANK
  pha
  stx $8000
  jsr jmp_to_ptr
  pla
  sta $8000
  rts

jmp_to_ptr:
  jmp (ptr)
To use it, set variable "ptr" to be the address of the subroutine you want to call, and then JSR to trampoline with the desired bank in register X. Like this:

Code: Select all

lda #.lobyte(foo2)
sta ptr+0
lda #.hibyte(foo)
sta ptr+1
ldx #2
jsr trampoline
Alternatively you can use a trampoline that isn't general purpose:

Code: Select all

.segment "FIXED_BANK"
foo2_trampoline:
  lda CURRENT_BANK
  pha
  lda #2
  sta $8000
  jsr foo2
  pla
  sta $8000
  rts
In this case you can just JSR to 'foo2_trampoline', which makes it really easy to use.
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to bank the code?

Post by rainwarrior »

Your routine is almost OK, just two more things would make it functional:

Don't do those two PLA instructions, you need to leave that to return to when done.

Also, you need one more wrapping layer of function call to return to the original bank. Something like:

0. JSR to the banked-call function. (Stack contains return address from the line after where you started the banked-call.) <return>
1. Store temporary parameters (A, in your case).
2. Push current bank number to stack. (We need to put it back afterwards.) <bank, return>
3. JSR to a "wrapper" function. (Pushes the line after this JSR to stack.) <banked-call in progress, bank, return>
4. Push subroutine address - 1 to stack. <subroutine, banked-call in progress, bank, return>
5. Write new bank number to mapper.
6. Reload temporary parameters.
6. RTS to go to the subroutine. <banked-call in progress, bank, return>
7. Subroutine eventually does its own RTS, returns to the line after wrapper function was called (3). <bank, return>
8. Pop the old bank number from the stack. <return>
9. Write old bank number to mapper.
10. RTS returns to line after we started. <>

(Trying to indicate the state of the stack with <>. Things your original code was missing in bold.)
Last edited by rainwarrior on Fri Nov 09, 2018 8:54 pm, edited 3 times in total.
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: How to bank the code?

Post by koitsu »

Let's not talk about imaginary mappers. Let's talk about real mappers, like say, MMC1, where the simplest form is 16KB PRG sizes, with $8000-BFFF swappable and $C000-FFFF fixed to the last bank. (Most mappers do have a fixed bank) Let's not talk about fake address ranges like $A000-FFFF (24KB? Hah!).

Now you know where your swap-PRG-bank routine should reside, to keep things simpler. It doesn't *have* to be this way, but you get to deal with complications (and sometimes copied code amongst banks) if you're trying to execute subroutines within banks that are being swapped. Is the latter doable? Yes. Complicated/annoying? Sometimes, yes. Worth it? Maybe, depends on your situation.

The same goes for the "main core" of your game/program.

If you can fit everything into the fixed bank (and many games do!), then you no longer have to worry about the situation you're worried about. Commercial games tend to do exactly this. 16KBytes is a pretty good chunk of space for code! With a fixed bank you also have a list of "static addresses/labels" that you can JMP to if there's an ugly situation you need to get out of.

If you're absolutely unconditionally hellbent on a mapper that swaps out the entire 32KB region of $8000-FFFF, then yes, you're going to have to deal with your exact situation. There are many ways to skin that cat. I'm just brain dumping stuff I've seen:

1) The way you described -- specifically using a temporary variable to keep track of the "old" bank, and put it back when done,

2) Design your banks so that there's duplicated code within them. Some commercial games do this; usually they dedicate "regions" that are duplicated across banks (common subroutines, etc.) so that they can swap PRG and do RTS/etc. without worrying too much.

3) Using JMP a lot more, rather than JSR/RTS. You can end up with a "big rat's nest" this way, depending on what it is you're doing. An alternate approach is to use an indirect jump (jmp ($xxxx)) where $xxxx is in RAM, and you manage the pointers yourself. (I'm currently dealing with a commercial game that does exactly this.)

4) A weird method involving putting code into RAM (usually just a few instructions) and running stuff out of there; often involves self-modifying code. I forget how this worked well for a particular game, but it's what the programmers went with regardless.

In general I think people get hung up on "trying to write something pretty", stemming from habits developed with higher-level languages and platforms where resources and space are more available. The NES isn't one of them. It's OK to write something that someone might consider "kludgy".
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to bank the code?

Post by rainwarrior »

koitsu wrote:If you're absolutely unconditionally hellbent on a mapper that swaps out the entire 32KB region of $8000-FFFF, then yes, you're going to have to deal with your exact situation.
OP's hypothetical is for a fixed bank configuration, not 32k. It's basically UxROM with no bus conflicts, if you ignore the (impossible) region addresses. (I'd maybe assume A000 was a typo for C000.)
Last edited by rainwarrior on Fri Nov 09, 2018 8:50 pm, edited 1 time in total.
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: How to bank the code?

Post by tepples »

koitsu wrote:Let's not talk about imaginary mappers. Let's talk about real mappers, like say, MMC1, where the simplest form is 16KB PRG sizes, with $8000-BFFF swappable and $C000-FFFF fixed to the last bank. (Most mappers do have a fixed bank) Let's not talk about fake address ranges like $A000-FFFF (24KB? Hah!).
MMC2 switches $8000-$9FFF (8 KiB) and leaves $A000-$FFFF (24 KiB) fixed. MMC4, on the other hand, has the more common UNROM-style 16+16 spilt for PRG ROM.
User avatar
koitsu
Posts: 4201
Joined: Sun Sep 19, 2004 9:28 pm
Location: A world gone mad

Re: How to bank the code?

Post by koitsu »

Good to know! *literally does not care about MMC2, or pedantry* (Edit: Comment was directed at tepples BTW, not rainwarrior :D)

I fully agree that we should probably put some effort into documenting some of these techniques, especially given how commonplace they are on the NES.
User avatar
rainwarrior
Posts: 8734
Joined: Sun Jan 22, 2012 12:03 pm
Location: Canada
Contact:

Re: How to bank the code?

Post by rainwarrior »

Lizard was 32K banking, which is basically why I did it that way.

1. No fixed bank meant that I wanted to duplicate the same code in several banks. I can't link code with the same name in multiple places, so this would have been tricky to solve with all of the banks in a single link step.

2. I was using the label files (-Ln) for debug symbols, which the omit segment/bank information I would have needed to separate them by bank. This could have been solved by using the debug file (--dbgfile) output instead, but I already had the (simpler) label file parsing done from previous work, and unless I could solve the problem 1 there wasn't any point to rewriting it.

So, that's the reason, but they wouldn't have applied to a mapper with a fixed bank. 1 is solvable; in MOON8 (source is at bottom of page) I did use a single link despite 32K banking, and duplicated code was accommodated by wrapping it in a scope for each bank, and using .include for the duplicated stuff instead of separate assembly objects. Either way has advantages and drawbacks, and there are a bunch of other ways to address this.

For 2, at the time I started Lizard, the debug file format was still being worked on in CC65, so I didn't want to use it. At this point it's settled down and I'll be using it instead for future projects.
tepples
Posts: 22708
Joined: Sun Sep 19, 2004 11:12 pm
Location: NE Indiana, USA (NTSC)
Contact:

Re: How to bank the code?

Post by tepples »

rainwarrior wrote:Lizard was 32K banking, which is basically why I did it that way.

1. No fixed bank meant that I wanted to duplicate the same code in several banks. I can't link code with the same name in multiple places, so this would have been tricky to solve with all of the banks in a single link step.
The workaround I use for this involves macros and .scope, as I demonstrate in my BNROM/AOROM template.
Post Reply