News: 11 March 2016 - Forum Rules, Mobile Version
Current Moderators - DarkSol, KingMike, MathOnNapkins, Azkadellia

Author Topic: FF1 MMC5 Conversion (Disassembly)  (Read 909 times)

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
FF1 MMC5 Conversion (Disassembly)
« on: September 23, 2017, 07:23:44 am »
I'm having trouble figuring out exactly how to set up this game to use the MMC5 mapper. I snooped around Disch's FF1 Namcot disassembly (found here: http://www.romhacking.net/forum/index.php?topic=24899.0 ) so I have a rough idea of the parts that need changing. And I have his mapper documents, and have read a few other forum threads... only to get more confused.

I decided on MMC5 for a few reasons:
Disch said it "wouldn't be the most difficult thing ever." :D
2 extra sound channels! Nice.
I do very much like the idea of the multiplication thing it can do.
I don't want to use the Namcot disassembly because its a little scary, and the disassembly is too different. Many labels are changed, things are uncommented, and on top of that, I don't know if I can use any extra RAM, or if it only works with sound stuff. I don't feel I need the super extra sound channels...

What I think I understand so far:
I need to set up the iNes header.
I need to set up the mapper inside "OnReset".
I need to set up how "SwapPRG" works differently.
If I use extra sound channels, I have to mute them separately when the game mutes the rest.

What I don't understand (potentially everything):
What values to use in setting up the iNes header.
Everything I need to initialize inside "OnReset"
How to set up "SwapPRG" at all.
If there is more I need to do with the amount of banks; nes.cfg and build.bat.

What I've done so far:
I've created a mostly-empty bank, Bank_0Z.asm, containing:
Code: [Select]
.include "Constants.inc"
.include "variables.inc"

.segment "BANK_0Z"
It is my understanding that this will fill itself up with 0s. My intention will be to swap to this bank when I need to do any custom code...

I've set up build.bat and nes.cfg the way Disch has it in the Namcot disassembly, replacing "musicengine.bin" with "Bank_0Z.asm". I'm using his filler.dat file as well. Bank F is set to the last bank still; I understand why this is necessary.

Do I need to make more "empty" banks, or change how big "filler.dat" is, to make sure the rom comes out to the right size? If I need to pad out filler.dat, how large should it be? It currently goes up to 000BC000--752 KB (3x the size of the original FF, minus the 16 KB for Bank Z). Am I off by 16 bytes or more?

To initialize the mapper in Bank F, I've done this so far:
Code: [Select]
OnReset:
    SEI            ; Set Interrupt flag (prevent IRQs from occuring)
    LDA #0
    STA $2000      ; Disable NMIs
    STA soft2000
    STA unk_FE     ; clear some PPU related areas in RAM
    LDA #$06
    STA $2001      ; disable Spr/BG rendering (shut off PPU)
    CLD            ; clear Decimal flag (just a formality, doesn't really do anything)

    LDX #$02       ; wait for 2 vblanks to occurs (2 full frames)
  @Loop:
      BIT $2002      ;  This is necessary because the PPU requires some time to "warm up"
      BPL @Loop      ;  failure to do this will result in the PPU basically not working
      DEX
      BNE @Loop

                ; X = 0
    STX $5100   ; 32k PRG-RAM (is this what I want?)
    STX $5101   ; 8k for CHR Mode Select (is this what I want?)
    INX         ; 01
    STX $5103   ; Allow writing to PRG-RAM B? 
    INX         ; 02
    STX $5102   ; Allow writing to PRG-RAM A?
    STX $5104   ; ExRAM mode Ex2   
       
    LDX #$44
    STX $5105       ; Hopefully, Vertical mirroring?

    LDA #0
    STA $4016      ; clear joypad strobe??  This seems like an odd place to do it since it doesn't read joy data here  =P
    STA $4015      ; turn off all sound channels
    STA $5015      ; turn off all sound channels (MMC5)
    STA $4010      ; disble DMC IRQs
    LDA #$C0
    STA $4017      ; set alternative pAPU frame counter method, reset the frame counter, and disable APU IRQs
   
    ;DEX            ; X was previously 0, this makes it FF
    LDX #$FF
    TXS            ; transfer it to the Stack Pointer (resetting the SP)
;   STX unk_0100   ; ??? (commented out in Namcot version, so I did it here too.)

    JSR ClearBGPalette ;  clear the BG palette

    LDA #$06           ; swap to bank 6 again (even though we just did) .. but why?  We never use
    JSR SwapPRG        ;   anything in bank 6
    JMP GameStart_L    ; jump to the start of the game!

I'm not sure if I'm missing things, or if I have the right settings for the registers I do have... There's a list in Disch's MMC5 document, but I don't know what's necessary.

For the "PRG_Swap", I don't know what to do at all.
The original has it STA-ing to $FFF9. From what I've read, it sounds like MMC5 uses either $5113, $5115, or even $5117? Is there more that needs to be done here, or is it as simple as STA and RTS?

I'm worried that even what I think I understand, I am massively confusing myself over...

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #1 on: September 23, 2017, 09:47:15 am »
I can't resist when I see a thread with "FF1" in the title.  I have to crawl out of the shadows.  :laugh:

Your brief list of the general things you need to do is correct.  Basically everything the original game does with MMC1, you have redo in MMC5 to have the same effect.  Fortunately, MMC1 doesn't do a whole lot, so the functionality is easy to mimic.  Unfortunately, MMC5 is a beast with dozens of registers, so working with it might seem overwhelming at first.


In general, on startup it's a good idea to write to every register at least once to put the mapper in a known state.  But this might be confusing because MMC5 has SO MANY registers... and not all of them are strictly necessary (some only are relevant in certain modes that you're not likely to be using)

So here's a minimal, rock-solid setup for FF1 using MMC5 that should work on all emulators.  Do these reg writes:

  -- Do these first --
$00 -> $5015   (disable MMC5 pulse channels)
$00 -> $5010   (disable MMC5 PCM and the IRQs it generates)
$00 -> $5204   (disable scanline IRQs)
$00 -> $5130   (hard to explain -- read the doc if you want know what this does)

  -- Do these in any order after that --
$FF -> $5117   (this actually probably isn't strictly necessary, but I would do it because I'm paranoid)
$01 -> $5100   (set PRG mode to 16K swap)
$02 -> $5102   (enable PRG-RAM)
$01 -> $5103   (ditto)
$04 -> $5113   (swap battery-backed PRG RAM into $6000 page.  You might be able to write $00 here, also, but $04 is probably a better idea)
$02 -> $5104   (turn off ExAttribute mode)
$00 -> $5101   (8K CHR swap mode --- FF1 doesn't do CHR swapping)
$00 -> $5127   (swap in the first CHR page)
  ** do not write to anything to $5128-512B... EVER **
$44 -> $5105   (set mirroring to vertical)
$00 -> $5200   (disable split-screen mode)




So that takes care of Reset -- now we just need PRG swapping!

Code: [Select]
PRG_Swap:
  ASL A       ; Double the page number (MMC5 uses 8K pages, but FF1 uses 16K pages)
  ORA #$80    ; Turn on the high bit to indicate we want ROM and not RAM
  STA $5115   ; Swap to the desired page
  LDA #0      ; IIRC Some parts of FF1 expect A to be zero when this routine exits
  RTS




As for the header --- the only thing you need to change is the mapper number (1 -> 5).  If you're also expanding the ROM, you'll also need to double the PRG size ($10 -> $20).

As for making empty banks (filler.dat) .. that's just to pad the ROM to the appropriate size.  It's only necessary if you're expanding.  If so, make it big enough so that your final ROM is exactly $80010 bytes (512K + 16 byte header)


That should be everything!

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #2 on: September 23, 2017, 12:15:03 pm »
IT WORKS.

YOU MAKE ME SO HAPPY!!

*wriggle wriggle* Thank you SO much! You just gave me the best birthday present!

One last thing I was wondering, what are the exact ranges for the extra RAM? I mean this stuff:
btl_attacker_strength = $686C

Would that be $5C00-5FFF ? Any register in there I can use to store stuff?

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #3 on: September 23, 2017, 01:59:50 pm »
Well how about that!  Happy birthday!   :beer:

And yes, $5C00-5FFF is all available RAM for you to use.

KingMike

  • Forum Moderator
  • Hero Member
  • *****
  • Posts: 6174
  • *sigh* A changed avatar. Big deal.
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #4 on: September 23, 2017, 02:14:18 pm »
As for making empty banks (filler.dat) .. that's just to pad the ROM to the appropriate size.  It's only necessary if you're expanding.  If so, make it big enough so that your final ROM is exactly $80010 bytes (512K + 16 byte header)


That should be everything!
Filler still needs to go between the boundary of the last 16KB ROM bank, doesn't it?
Quote
Sir Howard Stringer, chief executive of Sony, on Christmas sales of the PS3:
"It's a little fortuitous that the Wii is running out of hardware."

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #5 on: September 23, 2017, 06:51:29 pm »
Yes.  The last 16K still needs to remain the last 16K -- so the filler should go between that and the rest of the ROM.

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #6 on: September 24, 2017, 02:49:32 am »
Thanks!

One more problem popped up when I tried to build the file with $5C00-5FFF stuff... range error! I'm guessing I need to add something into nes.cfg to tell it that range is okay?

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #7 on: September 24, 2017, 10:29:14 am »
$5C00-5FFF is RAM, not ROM.  So the assembler doesn't need to know anything about it.  You don't need to make any changes to the config -- all you need to do is write to those addresses in your code -- or, if you want to use variable names, put "myvar = $5C00" somewhere in your code.

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #8 on: September 24, 2017, 07:18:51 pm »
That makes sense...! I mean, that's what I was doing...

I did... something... that cleared up most of the errors (that is, undoing all the tomfoolery I was doing for the last few months in Bank C). And my one important variable still works, so that's good.

Buuut if I change CHAN_START from $A0 to $5C00, I get errors. I was hoping to consolidate all the audio data in one area while I poke at the new square channels. Does this have to be in the System RAM area? I think I can squeeze it in if I have to (5 channels, 12 bytes each, from A0 to EF, with 4 left over...)

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #9 on: September 24, 2017, 08:28:35 pm »
CHAN_START probably has to be a zero page address, since it likely gets ADC'd at some point.

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #10 on: September 28, 2017, 11:39:39 am »
I can work with that. I think! We'll see.

So I tried my first bank swap attempt. Did not go well. But I learned! Now I'm wondering... Why can't I just do "LDA $0C" to return to Bank C after I swap to another one? The game only remembers its place if I do "LDA #BANK_THIS / STA cur_bank" and use "LDA cur_bank" to load it back up. Its being a silly billy about that.

At least I haven't run into something I haven't solved myself yet. :D



Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #11 on: September 28, 2017, 09:30:00 pm »
In general you can only swap from the fixed bank (bank F).

Code: [Select]
;  Assume this code is in a swappable bank (not bank F)
LDA #$0C
JSR PRG_Swap

; PRG_Swap will write to the MMC5 regs to swap out the PRG, then will RTS.
;  ... but where will it RTS to?  Certainly not here, because this bank is
;  no longer swapped in -- you just swapped it out.
;
; So it'll jump back to this address in another bank, which is likely to land
;  in the middle of nonsense and crash the game.

There are ways to cheese this if you want to get clever with the stack -- and they involve pushing the desired return address (minus one) to the stack before swapping in a bank, then JMPing to PRG_Swap rather than JSRing.

I don't recommend doing this unless you really know what you're doing.. as it's very easy to get wrong and is kind of confusing.  But if you're curious....

Code: [Select]
; In bank 0  (swappable), I want to call 'SomeRoutine' in bank 1, and have it jump
;   back here when it's done
LDA #>(jump_back_to-1)  ; push high byte of return address first
PHA
LDA #<(jump_back_to-1)  ; push low byte
PHA

LDA #>(SomeRoutine-1)   ; push high byte of target address
PHA
LDA #<(SomeRoutine-1)   ; then low byte
PHA

LDA #1
JMP PRG_Swap            ; Swap to bank 1.. the RTS will "return" to the start of SomeRoutine,
                        ;   since that's the address at the top of the stack

jump_back_to:
  ; SomeRoutine will return here when it exits


;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

; In bank 1  (swappable)

SomeRoutine:
   ; ... do some stuff.  When you're done, don't just RTS out, instead do this:

   LDA #0
   JMP SwapPRG    ; Will swap in bank 0... the RTS will "return" to jump_back_to, since
                  ;   that is now at the top of the stack

Like I said... confusing.  Probably not worth it.  Just keep your calls to PRG_Swap in the fixed bank.




OR... since MMC5 is capable of 8K page swapping, you can switch to 8K mode and swap out 8K instead of the whole 16K ... but then you'd have to change PRG_Swap and some other stuff and... yeah.... just keep your swaps in the fixed bank  =P

Jiggers

  • Jr. Member
  • **
  • Posts: 52
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #12 on: September 28, 2017, 10:51:41 pm »
Ooh, neat. I see what I did wrong.

I was doing this in Bank Z, to go back to Bank C...
LDA #$0C
JMP SwapPRG

But my first swap was in the fixed bank, so that's why that part worked.

An idea: Save cur_bank during SwapPRG?
Code: [Select]
SwapPRG:
  PHA          ; push next-bank to stack
  LDA $5115    ; load current bank???
  STA cur_bank ; save current bank
  PLA          ; pull next-bank from stack
  ASL A       ; Double the page number (MMC5 uses 8K pages, but FF1 uses 16K pages)
  ORA #$80    ; Turn on the high bit to indicate we want ROM and not RAM
  STA $5115   ; Swap to the desired page
  LDA #0      ; IIRC Some parts of FF1 expect A to be zero when this routine exits
  RTS

...gotta be more complex than that... 'cos that don't work.

Is there a way to turn a label into a variable? Although, thinking about it, I don't think it'd be the best idea space-wise. Even if its as simple as
Code: [Select]
    LDA #>LabelName
    STA SavedLabel
    LDA #<LabelName
    STA SavedLabel+1
That's 8, 10? bytes - in the bank I want to save space in. So my current plan to shove as much into the Fixed and Z banks is probably still the best one.
« Last Edit: September 29, 2017, 09:52:48 am by Jiggers »

Disch

  • Hero Member
  • *****
  • Posts: 2539
  • NES Junkie
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #13 on: September 29, 2017, 10:43:27 am »
A few notes:

1)  $5115 is write-only and is not readable.  In fact you can't count on any reg being readable unless it's explicitly stated to be readable.

2)  That wouldn't work anyway since cur_bank has a 16K bank number (needs to be left-shifted) and $5115, if it worked how you wanted, would give you an 8k bank number (already left-shifted).

3)  To add to the confusion, I don't think cur_bank is always strictly the current bank =x.  I think it's the bank with the main game logic that the game is focusing on.  IIRC it's used by the music routine -- and it's basically "swap back to cur_bank after music code finishes".  This should be *mostly* reliable... but there are a few times when this will get you the wrong bank (like when the game is loading graphics from a bank that doesn't have any code in it -- or when the music routine is running)


Quote
Is there a way to turn a label into a variable?

That's worded very strangely.  =P
If you're asking if there's a way to save the address of the start of a routine to a variable, then yes -- and your sample code is exactly how you'd do it.

Since I've got a few minutes... here's something for you that might work.  It's untested, so it might be buggy -- but hopefully you'll get the idea of what it's doing:


Code: [Select]
;;;;;;;;;;;;;;;
;;  In any swappable bank

    ; Let's say I want to call 'SomeRoutine' in bank 5, but I'm currently in a different bank.
    ;   I can just do this:
    JSR LongCall
        .WORD SomeRoutine       ; the routine to jump to
        .BYTE 5                 ; the bank it's in
       
    LDA whatever        ; <- code will resume here when SomeRoutine finishes

How to make this magic work????!?!?!  It's messy:

Code: [Select]
;;;;;;;;;;;;;;;
;;  In the fixed bank:

PRG_Swap:
    STA actual_bank         ; make this a new variable somewhere.  This can be in your $5Cxx page if you want
   
    ; ... do the rest of the normal swap stuff here
   
    RTS
   
   
;;;;;;;;;;;;;;;;

LongCall:
    @return_addr = tmp      ; make this any 2 bytes of available memory.  Note that this MUST be in zero page
                            ;   Also note that this memory must not be overwritten by the routine we're calling,
                            ;   so 'tmp' might not be the best choice --- but it'll work as long as you're careful
    @jump_addr = tmp+2      ; make this any 2 bytes of available memory (doesn't need to be zero page)
                           
    PLA             ; pull low byte of return address
    STA @return_addr
    PLA             ; then high byte
    STA @return_addr+1
   
    ; @return_addr now has the return address (minus 1).  We can now read some bytes
    ;   from that to get the desired jump-to address
   
    LDY #1                  ; Y starts at 1 because the return address is actually -1
    LDA (@return_addr),Y    ; read low byte of target address
    STA @jump_addr
   
    INY
    LDA (@return_addr),Y    ; read high byte of target address
    STA @jump_addr+1
   
    ; @jump_addr now has the address we want to jump to
    LDA actual_bank     ; get the 'actual' current bank
    PHA                 ;  back it up
   
    INY
    LDA (@return_addr),Y ; get the target bank
    JSR PRG_Swap        ; swap to it
    JSR @doLongCall     ; call our target routine
   
    PLA
    JSR PRG_Swap        ; swap the original bank in
   
    ; add 3 to our return address (to skip the 3 bytes we read)
    CLC
    LDA @return_addr
    ADC #3
    STA @return_addr
    LDA @return_addr+1
    ADC #0
    PHA                 ; push high byte to stack
    LDA @return_addr
    PHA                 ; then low byte
   
    RTS                 ; return!

    @doLongCall:
        JMP (@jump_addr)


It'll take a sizeable chunk of space in the fixed bank... but once you have that you'll be able to make routine calls from any bank to any other bank.




EDIT

So I optimized the LongCall routine a bit.

Things that are better:
- @return_addr no longer needs to be persistent.  It's just temp ram and code outside the routine can overwrite it
- This also makes LongCall re-entrant, where it wasn't before (ie:  a routine that is LongCall'd can do another LongCall inside it.  Previously this would break because @return_addr would get corrupted)
- Size is reduced to 46 bytes instead of 53
- Probably a speed increase as well but I don't want to count cycles

Things that are worse:
- It now uses A, Y, and X.  The old routine left X untouched.
- I didn't bother commenting



Things that are the same:
- It's called the same way (with the .WORD & .BYTE lines after the JSR)
- It still relies on 'actual_bank' being set inside of PRG_Swap
- It must live in the fixed bank.
- I still didn't actually test it, but I'm reasonably confident it will work



The new code:
Code: [Select]
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;  Re-entrant Long Call

LongCall:
    @return_addr = tmp      ; (2 bytes) Must be on zero page
    @jump_addr = tmp+2      ; (2 bytes) can be anywhere -- bytes cannot cross page boundry
   
    PLA
    STA @return_addr
    CLC
    ADC #3
    TAY
    PLA
    STA @return_addr+1
    ADC #0
    PHA
    TYA
    PHA
   
    LDY #1
    LDA (@return_addr),Y
    STA @jump_addr
    INY
    LDA (@return_addr),Y
    STA @jump_addr+1
    INY
   
    LDA actual_bank
    PHA
    LDA (@return_addr),Y
    JSR PRG_Swap
    JSR @doLongCall
   
    PLA
    JMP PRG_Swap
   
@doLongCall:
    JMP (@jump_addr)



EDIT AGAIN:

I just realized there's no reason to use X.  I can just use Y.  Above routine updated.  This also means the new routine is 100% superior to the old one.
« Last Edit: October 01, 2017, 01:45:48 pm by Disch »

EntroperZero

  • Jr. Member
  • **
  • Posts: 9
    • View Profile
Re: FF1 MMC5 Conversion (Disassembly)
« Reply #14 on: November 11, 2017, 02:43:40 am »
Hey guys,

Thanks a million for this thread.  :)  I wanted to try doing exactly the same thing, switching the mapper to MMC5 and expanding the ROM.  I had mostly figured it out thanks to Disch's documentation of the MMC5, but this thread made it even easier.

For some reason, Higan doesn't like the ROM.  But it works fine in NesHawk and FCEUX.  Next is to see if the flashcarts will support it.  I don't plan on using the MMC5's features other than the additional PRG ROM banks, so fingers crossed.  Ideally, the ExRAM will work too, because I have ideas for how to use it, but we'll see.

For reference, here's all that's necessary to expand the ROM to 512 kB:

Code: [Select]
public void UpgradeToMMC5()
{
// See https://www.romhacking.net/forum/index.php?topic=24989.0

Header[4] = 32; // 32 pages of 16 kB
Header[6] = 0x53; // original is 0x13, we're changing the 1 (MMC1) to a 5 (MMC5)

Blob newData = new byte[0x80000];
Array.Copy(Data, newData, 0x3C000);
Array.Copy(Data, 0x3C000, newData, 0x7C000, 0x4000);
Data = newData;

// Initialize MMC5
Put(0x7FE48, Blob.FromHex("a9008d15508d10508d04528d30518d01518d27518d0052a9ff8d1751a9018d00518d03510a8d02518d04510a8d1351a9448d0551a900"));

// Change bank swap code
Put(0x7FE1A, Blob.FromHex("0a09808d1551a90060"));
}