Page 1 of 1

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:28 pm
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.

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:33 pm
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.)

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:39 pm
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".

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:43 pm
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.)

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:46 pm
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.

Re: How to bank the code?

Posted: Fri Nov 09, 2018 8:49 pm
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.

Re: How to bank the code?

Posted: Fri Nov 09, 2018 9:28 pm
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.

Re: How to bank the code?

Posted: Fri Nov 09, 2018 9:35 pm
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.