It is currently Tue Nov 20, 2018 4:24 pm

All times are UTC - 7 hours





Post new topic Reply to topic  [ 11 posts ] 
Author Message
 Post subject: How to bank the code?
PostPosted: Fri Nov 09, 2018 8:11 pm 
Offline

Joined: Tue Aug 28, 2018 8:54 am
Posts: 71
Location: Edmonton, Canada
I honestly tried search for "bank" and "banking" but could not find the answer I was looking for, and there is something missing in my head to grok it.

I have imaginary mapper:
1. fixed A000-FFFF bank
2. two variable banks at 8000-BFFF.
3. write 1 or 2 at 8000-FFFF will switch banks (no bus conflicts)
4. always start at bank 1

And now I'm in the bank 1 and I want to call routine 'foo' in bank too.

Code:
jsr foo1
jsr foo2 ; <-- foo2 is in bank 2


Okay, so I can switch banks.

Code:
8100:   jsr foo1
8102:   lda #2
8104:   sta $8000
; but it will not jump to the foo2 because code at $8106
; in second bank is completely different (or it is data)
8106:   jsr foo2 ; <-- foo2 is in bank 2


Okay, so I understand _how_ banking work, my problem is what is the optimal approach. I can come up with something like this:

Code:
jsr foo1
ldx #<foo2-1
ldy #>foo2-1
lda #2
jsr switch_bank

.proc switch_bank:
   sta tmp1
   ; take out return address
   pla
   pla
   ; push method address on the stack
   txa
   pha
   tya
   pha
   ; and return there
   rts
.endproc


But now I lost original return address. Okay, save the return address and write some king of bank_rts and switch bank to the previous bank on the return. But it seems quite cumbersome.

And as an additional trouble, I use ca65 that uses C-style linking and using '.org' is not really a right way to go. And I _must_ have my 'switch_bank' on the same address.

Does anyone have any advice on that? What are common/optimal approaches? I don't even need the code, but process. Thank you, and sorry for dumb questions here.

P.S. I would really love to have wiki page with options of how banking can be implemented.


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:28 pm 
Offline
User avatar

Joined: Thu Mar 31, 2016 11:15 am
Posts: 423
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:
.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:
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:
.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.


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:33 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 6961
Location: Canada
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.

Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:39 pm 
Offline
User avatar

Joined: Sun Sep 19, 2004 9:28 pm
Posts: 3694
Location: Mountain View, CA
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".


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:43 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 6961
Location: Canada
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.

Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:46 pm 
Offline

Joined: Sun Sep 19, 2004 11:12 pm
Posts: 20789
Location: NE Indiana, USA (NTSC)
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.


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 8:49 pm 
Offline
User avatar

Joined: Sun Sep 19, 2004 9:28 pm
Posts: 3694
Location: Mountain View, CA
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.


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 9:04 pm 
Offline

Joined: Tue Aug 28, 2018 8:54 am
Posts: 71
Location: Edmonton, Canada
koitsu wrote:
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.


I don't have enough code/data to fill one bank, but I'm sure I will. But I would still need to jump into the proper place. Plus some code will probably be too big to duplicate, and better just keep it in one bank.

PS address ranges are typo, I was intending to split it in 16kb.

rainwarrior wrote:
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...


Thank you, I think I understand exactly what to do. I know you work with ca65 a lot. I was looking at the lizard source, and it did not really used linker to the full capacity and was just merging binaries manually with copy. I failed (or mostly not spend enough time) how it work exactly. Were you limited by linker so you had to hack it this way?


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 9:28 pm 
Offline
User avatar

Joined: Sun Jan 22, 2012 12:03 pm
Posts: 6961
Location: Canada
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.


Top
 Profile  
 
PostPosted: Fri Nov 09, 2018 9:35 pm 
Offline

Joined: Sun Sep 19, 2004 11:12 pm
Posts: 20789
Location: NE Indiana, USA (NTSC)
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.


Top
 Profile  
 
PostPosted: Sat Nov 10, 2018 2:40 pm 
Offline

Joined: Tue Aug 28, 2018 8:54 am
Posts: 71
Location: Edmonton, Canada
tepples, I am looking through your code. Why do you use 'sta *-1' and not 'sta *' or 'sta $8000'?

edit: I just realized. Because *-1 contains the value of the bank, and you don't need to use 'identity16,x' for that. That's smart, I like it.


Top
 Profile  
 
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 11 posts ] 

All times are UTC - 7 hours


Who is online

Users browsing this forum: Google [Bot] and 1 guest


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
Powered by phpBB® Forum Software © phpBB Group