emulate mapper 1 for dragon warrior 3 problem

Discuss emulation of the Nintendo Entertainment System and Famicom.

Moderator: Moderators

Post Reply
ryunes
Posts: 4
Joined: Sat Mar 25, 2023 7:59 am

emulate mapper 1 for dragon warrior 3 problem

Post by ryunes »

I'm writing an emulator for nes. Now it supports mapper 0, 2, 3 quite well. I'm trying to support mapper 1, and some roms run well too, such as zelda, dragon warrior 2(USA), mega man 2 and so on.

But it crash on Dragon warrior 3 (USA) (invalid opcode 0x9f) .

It seems that I make some mistake on PRG bank switching. But I don't know where.

Here is my Mapper reading and writing code for mapper 1.

Yes I haven't support the PRG RAM yet (But this does not affect the crash bug).

Code: Select all

static void INesMapper1Write(INesInstance* instance, uint16_t addr, uint8_t data) {
    assert(addr >= 0x6000 || addr < 0x2000);
    assert(instance->mapper->number == (uint8_t)INesMapperTypeMMC1);
    if (addr >= 0x8000) {
        if ((data >> 7) == 1) {
            instance->mapper->reg0 = 0x10;
            instance->mapper->reg1 |= 0xc;
        } else {
            uint8_t complete = instance->mapper->reg0 & 1;
            instance->mapper->reg0 >>= 1;
            instance->mapper->reg0 |= (data & 1) << 4;
            printf("write $%04x\n", addr);
            if (complete) {
                if (addr <= 0x9fff) {
                    // control
                    instance->mapper->reg1 = instance->mapper->reg0 & 0x1f;
                    uint8_t mirror = instance->mapper->reg1 & 3;
                    if (mirror <= 1) {
                        instance->mirror = INesInstanceMirrorOneScreen;
                    } else if (mirror == 2) {
                        instance->mirror = INesInstanceMirrorVertical;
                    } else {
                        assert(mirror == 3);
                        instance->mirror = INesInstanceMirrorHorizontal;
                    }
                } else if (addr <= 0xbfff) {
                    // chr bank 0
                    printf("bank 0 = $%02x\n", instance->mapper->reg0);
                    instance->mapper->reg2 = instance->mapper->reg0 & 0x1f;
                } else if (addr <= 0xdfff) {
                    // chr bank 1
                    instance->mapper->reg3 = instance->mapper->reg0 & 0x1f;
                } else {
                    assert(addr <= 0xffff);
                    // prg bank
                    instance->mapper->reg4 = instance->mapper->reg0 & 0x0f;
                }
                instance->mapper->reg0 = 0x10;
            }
        }
    } else if (addr >= 0x6000) {
        instance->mem[addr] = data;
    } else if (addr < 0x2000) {
        // CHR
        instance->ppu->mem[addr] = data;
    }
}
static uint8_t INesMapper1Read(INesInstance* instance, uint16_t addr) {
    assert(addr >= 0x6000 || addr < 0x2000);
    if (addr >= 0x8000) {
        size_t startOffset = 0;
        size_t spaceSize = instance->file->PRGRomSize;
        uint32_t bank = instance->mapper->reg4;
        if (instance->file->PRGRomSize == 524288) {
            if (instance->mapper->reg2 & 0x10) {
                startOffset = 262144;
            }
            spaceSize = 262144;
        }
        uint8_t prgBankMode = (instance->mapper->reg1 >> 2) & 3;
        if (prgBankMode <= 1) {
            return instance->file->PRGRom[startOffset + ((bank & 0xe) << 14) + (uint32_t)(addr - 0x8000)];
        } else if (prgBankMode == 2) {
            if (addr < 0xc000) {
                // fix first bank at $8000
                return instance->file->PRGRom[startOffset + addr - 0x8000];
            } else {
                assert(addr >= 0xc000);
                return instance->file->PRGRom[startOffset + (bank << 14) + (uint32_t)(addr - 0xc000)];
            }
        } else if (prgBankMode == 3) {
            if (addr >= 0xc000) {
                // fix last bank at $C000
                return instance->file->PRGRom[startOffset + spaceSize - 0x4000 + ((uint32_t)addr - 0xc000)];
            } else {
                assert(addr < 0xc000);
                return instance->file->PRGRom[startOffset + (bank << 14) + (uint32_t)(addr - 0x8000)];
            }
        }
    } else if (addr >= 0x6000) {
        return instance->mem[addr];
    } else if (addr < 0x2000) {
        if (instance->file->CHRRomSize == 0) {
            return instance->ppu->mem[addr];
        }
        
        if ((instance->mapper->reg1 >> 4) & 1) {
            // 4 kb mode, switch two separate 4 KB banks
            if (addr < 0x1000) {
                // lower bank
                uint32_t cvtaddr = ((uint32_t)instance->mapper->reg2 << 12) + (uint32_t)addr;
                if (cvtaddr >= instance->file->CHRRomSize) {
                    assert(!"chr size error!");
                    return 0;
                }
                return instance->file->CHRRom[cvtaddr];
            } else {
                // upper bank
                uint32_t cvtaddr = ((uint32_t)instance->mapper->reg3 << 12) + ((uint32_t)addr - (uint32_t)0x1000);
                if (cvtaddr >= instance->file->CHRRomSize) {
                    assert(!"chr size error!");
                    return 0;
                }
                return instance->file->CHRRom[cvtaddr];
            }
        } else {
            // 8 kb mode, switch 8 KB at a time
            uint32_t cvtaddr = ((uint32_t)(instance->mapper->reg2 & 0xfe) << 12) + (uint32_t)addr;
            if (cvtaddr >= instance->file->CHRRomSize) {
                assert(!"chr size error!");
                return 0;
            }
            return instance->file->CHRRom[cvtaddr];
        }
    }
    return 0;
}
User avatar
Dwedit
Posts: 4924
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Re: emulate mapper 1 for dragon warrior 3 problem

Post by Dwedit »

In order to support a cartridge size larger than 256K, Dragon Warrior 3 and 4 use the NES-SUROM board. This wires the MMC1 in an unusual way so that the top CHR selection bit is actually a "which 256k is selected" bit for the PRG.

Edit:

Now I'm actually reading your code, and it does take the SUROM mapper into account.

Maybe post a trace log? Or at least the list of mapper writes and their values, and which PC it crashes at?
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!
ryunes
Posts: 4
Joined: Sat Mar 25, 2023 7:59 am

Re: emulate mapper 1 for dragon warrior 3 problem

Post by ryunes »

Dwedit wrote: Sat Mar 25, 2023 11:12 am In order to support a cartridge size larger than 256K, Dragon Warrior 3 and 4 use the NES-SUROM board. This wires the MMC1 in an unusual way so that the top CHR selection bit is actually a "which 256k is selected" bit for the PRG.

Edit:

Now I'm actually reading your code, and it does take the SUROM mapper into account.

Maybe post a trace log? Or at least the list of mapper writes and their values, and which PC it crashes at?
Thanks for you reply. I print the trace log and find the invalid opcode happend when chr bank 0 switch from 0x10 to 0x00. Here's the log:

nes mapper1 write $ffdf $81
nes mapper1 write $ffdf $81
nes mapper1 write $9fff $0e
nes mapper1 write $9fff $07
nes mapper1 write $9fff $03
nes mapper1 write $9fff $01
nes mapper1 write $9fff $00
nes mapper1 write $bfff $10
nes mapper1 write $bfff $08
nes mapper1 write $bfff $04
nes mapper1 write $bfff $02
nes mapper1 write $bfff $01
chr bank 0 write $10
nes mapper1 write $dfff $00
nes mapper1 write $dfff $00
nes mapper1 write $dfff $00
nes mapper1 write $dfff $00
nes mapper1 write $dfff $00
nes mapper1 write $ffff $01
nes mapper1 write $ffff $00
nes mapper1 write $ffff $00
nes mapper1 write $ffff $00
nes mapper1 write $ffff $00
nes mapper1 write $bfff $00
nes mapper1 write $bfff $00
nes mapper1 write $bfff $00
nes mapper1 write $bfff $00
nes mapper1 write $bfff $00
chr bank 0 write $00
nes mapper1 write $6a3d $00

And then it happen when the pc = 0xe827. It's really weird.
User avatar
Dwedit
Posts: 4924
Joined: Fri Nov 19, 2004 7:35 pm
Contact:

Re: emulate mapper 1 for dragon warrior 3 problem

Post by Dwedit »

A quick log of what happens in FCEUX (where the game works)

>0F:FFD9: EE DF FF INC $FFDF = #$80 ; resets MMC1

>0F:C9EA: EE DF FF INC $FFDF = #$80 ; resets MMC1

;A = 0x0E
;Register 9FFF = Control. Value 0E means 8K CHR banking mode, last PRG bank fixed at C000 and switchable at 8000, Vertical Mirroring
>0F:C651: 8D FF 9F STA $9FFF = #$8A ; writes 0
0F:C654: 4A LSR
0F:C655: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C658: 4A LSR
0F:C659: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C65C: 4A LSR
0F:C65D: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C660: 4A LSR
0F:C661: 8D FF 9F STA $9FFF = #$8A ; writes 0
0F:C664: 60 RTS -----------------------------------------
Returns to Bank 0F, C9F3

;A = 0x10
;Register BFFF = CHR Bank 0. Setting the high bit (0x10) selects the second 256K for PRG.
>0F:C668: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C66B: 4A LSR
0F:C66C: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C66F: 4A LSR
0F:C670: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C673: 4A LSR
0F:C674: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C677: 4A LSR
0F:C678: 8D FF BF STA $BFFF = #$BF ;writes 1
0F:C67B: 60 RTS -----------------------------------------
Returns to Bank 1F, C9F9

;A = 0x00
;Register DFFF = CHR Bank 1. Should be ignored for 8K CHR mode.
>1F:C67C: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C67F: 4A LSR
1F:C680: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C683: 4A LSR
1F:C684: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C687: 4A LSR
1F:C688: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C68B: 4A LSR
1F:C68C: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C68F: 60 RTS -----------------------------------------
Returns to Bank 1F, E819

;A = 0x01
;Register FFFF = PRG Bank.
>1F:FFAC: 8D FF FF STA $FFFF = #$E8 ; writes 1
1F:FFAF: 4A LSR
1F:FFB0: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFB3: 4A LSR
1F:FFB4: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFB7: 4A LSR
1F:FFB8: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFBB: 4A LSR
1F:FFBC: 8D FF FF STA $FFFF = #$E8 ; writes 0
;this function also performs a CHR bankswitch to pick which 256K or PRG we want to use. This is setting it back to 0.
1F:FFBF: AD C8 06 LDA $06C8 = #$00
1F:FFC2: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFC5: 4A LSR
1F:FFC6: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFC9: 4A LSR
1F:FFCA: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFCD: 4A LSR
1F:FFCE: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFD1: 4A LSR
1F:FFD2: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFD5: EA NOP
1F:FFD6: EA NOP
1F:FFD7: 60 RTS -----------------------------------------
Returns to bank 0F, E81E

>0F:E81E: A9 00 LDA #$00
0F:E820: 8D 15 40 STA APU_STATUS = #$00
0F:E823: 8D 3D 6A STA $6A3D = #$00
0F:E826: 00 BRK
0F:E827: 9F UNDEFINED
0F:E828: 07 UNDEFINED

If you aren't emulating the BRK instruction correctly, you're going to have problems here. The game uses BRK as a way to perform off-page jumps or calls. The IRQ handler takes over performs all the code necessary to switch banks and jump to the target function. The BRK instruction does not care if interrupts are disabled or not, it always proceeds to the IRQ handler.

Upon executing the BRK, stack pointer moves from 1FF to 1FC, and values 36 (processor status), 28 (low byte of 'return address'), E8 (high byte of 'return address') are on the stack. Note that the return address is one byte after the instruction following the BRK instruction. Even though BRK is at E826, the value on the stack is actually E828.

So in summary, looks like Mapper 1 is fine enough for this game, Though it will break on the games "Bill & Ted" or "Shinsenden" which rely on obscure interactions of the MMC1 and the CPU's Read-Modify-Write instructions (like INC abs).
Here come the fortune cookies! Here come the fortune cookies! They're wearing paper hats!
ryunes
Posts: 4
Joined: Sat Mar 25, 2023 7:59 am

Re: emulate mapper 1 for dragon warrior 3 problem

Post by ryunes »

Dwedit wrote: Sun Mar 26, 2023 10:52 am A quick log of what happens in FCEUX (where the game works)

>0F:FFD9: EE DF FF INC $FFDF = #$80 ; resets MMC1

>0F:C9EA: EE DF FF INC $FFDF = #$80 ; resets MMC1

;A = 0x0E
;Register 9FFF = Control. Value 0E means 8K CHR banking mode, last PRG bank fixed at C000 and switchable at 8000, Vertical Mirroring
>0F:C651: 8D FF 9F STA $9FFF = #$8A ; writes 0
0F:C654: 4A LSR
0F:C655: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C658: 4A LSR
0F:C659: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C65C: 4A LSR
0F:C65D: 8D FF 9F STA $9FFF = #$8A ; writes 1
0F:C660: 4A LSR
0F:C661: 8D FF 9F STA $9FFF = #$8A ; writes 0
0F:C664: 60 RTS -----------------------------------------
Returns to Bank 0F, C9F3

;A = 0x10
;Register BFFF = CHR Bank 0. Setting the high bit (0x10) selects the second 256K for PRG.
>0F:C668: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C66B: 4A LSR
0F:C66C: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C66F: 4A LSR
0F:C670: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C673: 4A LSR
0F:C674: 8D FF BF STA $BFFF = #$BF ; writes 0
0F:C677: 4A LSR
0F:C678: 8D FF BF STA $BFFF = #$BF ;writes 1
0F:C67B: 60 RTS -----------------------------------------
Returns to Bank 1F, C9F9

;A = 0x00
;Register DFFF = CHR Bank 1. Should be ignored for 8K CHR mode.
>1F:C67C: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C67F: 4A LSR
1F:C680: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C683: 4A LSR
1F:C684: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C687: 4A LSR
1F:C688: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C68B: 4A LSR
1F:C68C: 8D FF DF STA $DFFF = #$E6 ; writes 0
1F:C68F: 60 RTS -----------------------------------------
Returns to Bank 1F, E819

;A = 0x01
;Register FFFF = PRG Bank.
>1F:FFAC: 8D FF FF STA $FFFF = #$E8 ; writes 1
1F:FFAF: 4A LSR
1F:FFB0: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFB3: 4A LSR
1F:FFB4: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFB7: 4A LSR
1F:FFB8: 8D FF FF STA $FFFF = #$E8 ; writes 0
1F:FFBB: 4A LSR
1F:FFBC: 8D FF FF STA $FFFF = #$E8 ; writes 0
;this function also performs a CHR bankswitch to pick which 256K or PRG we want to use. This is setting it back to 0.
1F:FFBF: AD C8 06 LDA $06C8 = #$00
1F:FFC2: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFC5: 4A LSR
1F:FFC6: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFC9: 4A LSR
1F:FFCA: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFCD: 4A LSR
1F:FFCE: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFD1: 4A LSR
1F:FFD2: 8D FF BF STA $BFFF = #$BF ; writes 0
1F:FFD5: EA NOP
1F:FFD6: EA NOP
1F:FFD7: 60 RTS -----------------------------------------
Returns to bank 0F, E81E

>0F:E81E: A9 00 LDA #$00
0F:E820: 8D 15 40 STA APU_STATUS = #$00
0F:E823: 8D 3D 6A STA $6A3D = #$00
0F:E826: 00 BRK
0F:E827: 9F UNDEFINED
0F:E828: 07 UNDEFINED

If you aren't emulating the BRK instruction correctly, you're going to have problems here. The game uses BRK as a way to perform off-page jumps or calls. The IRQ handler takes over performs all the code necessary to switch banks and jump to the target function. The BRK instruction does not care if interrupts are disabled or not, it always proceeds to the IRQ handler.

Upon executing the BRK, stack pointer moves from 1FF to 1FC, and values 36 (processor status), 28 (low byte of 'return address'), E8 (high byte of 'return address') are on the stack. Note that the return address is one byte after the instruction following the BRK instruction. Even though BRK is at E826, the value on the stack is actually E828.

So in summary, looks like Mapper 1 is fine enough for this game, Though it will break on the games "Bill & Ted" or "Shinsenden" which rely on obscure interactions of the MMC1 and the CPU's Read-Modify-Write instructions (like INC abs).
Thanks ! You are absolutely right. It is the BRK implementation bug. I wonder why the nestest didn't find it out.
Post Reply