It is currently Tue Nov 12, 2019 4:02 pm

All times are UTC - 7 hours





Post new topic Reply to topic  [ 3 posts ] 
Author Message
 Post subject: MMC5 EXRAM
PostPosted: Fri Nov 01, 2019 11:29 am 
Offline
User avatar

Joined: Sat Jun 08, 2019 2:53 am
Posts: 22
Location: Portland, OR
So I've been working on finishing up my MMC5 implementation for several days and I can't seem to grasp how EXRAM fully works.

I can play Castlevania III all the way through and even the water rising after the boss in stage 06 fills up with water like it's supposed to.

Uncharted Waters loads and seems to run fine (meaning my PRG swapping is working) but the intro BG tiles are all wrong. The book sprite seems to load fine during the intro scroll and I can see from my PPU debugger that the text appears in the nametable (one line at a time), but then disappears right when it's about to scroll into view.

Just Breed shows sprites properly once I start the game, but the BG is all messed up.

I know it's how I'm handling EXRAM with Extended Attributes, but I can't seem to figure out what I'm doing wrong with it. I know if you don't do EXRAM at all you'll get a whole screen of garbage tiles in Uncharted Waters during the intro instead of a black background.

I've searched around on the forums and this post viewtopic.php?f=3&t=10995&hilit=exram&start=15 seemed to be the most helpful, but I still can't get things to work. I'm wondering if a second set of eyes might reveal what I'm missing.

Here are the poignant chunks of code for reference (I'm sure there are cleaner ways to accomplish some things, but i don't want to mess with it until it's working). Sorry, I know it's a lot to pour over and the forums don't have great syntax highlighting, but I'm at my wits end.

I think my next debugging step is going to have to be installing a VM with Windows (I run on macOS) so I can run Mesen's debugger or something to try and do a side-by-side comparison of EXRAM read/writes to figure out where I'm going wrong.

Hopefully someone has experienced something similar enough to help! Much appreciated!

Source is also on github (if that's easier):
https://github.com/lukexor/rustynes/blo ... om.rs#L205
https://github.com/lukexor/rustynes/blo ... u.rs#L1520

Inside my ExROM code:
Code:
    // Gets the bank mapped CHR ROM address
    fn get_chr_addr(&self, addr: u16) -> usize {
        // EXRAM Mode 1 = Extended Atribute mode
        // Only return 20 bit CHR ROM address during BG fetches
        // 32 BG tiles = 32 * 4 = 128 (start of SPR fetch)
        // 8 SPR tiles = 8 * 4 = 32 + 128 = 160 (end of SPR fetch)
        if self.regs.exram_mode == 1 && (self.spr_fetch_count < 127 || self.spr_fetch_count > 159) {
            let hibits = (self.regs.chr_hi_bit as usize) << 18;
            let exbits = (self.exram.peek(addr % 0x0400) as usize & 0x3F) << 12;
            hibits | exbits | (addr as usize) & 0x0FFF
        } else {
            // 8K, 4K, 2K, or 1K bank sizes
            let bank_size = (8 * 1024) / (1 << self.regs.chr_mode as usize);
            let offset = addr as usize % bank_size;
            // Corresponds to regs $5121 - $5127
            // BG only has half the banks as SPR, so we can AND this with 0x03
            let bank_idx = match self.regs.chr_mode {
                0 => 7,
                1 => 3 + ((addr >> 12) << 2),
                2 => 1 + ((addr >> 11) << 1),
                3 => addr >> 10,
                _ => panic!("invalid chr_mode"),
            } as usize;
            let bank = if self.regs.sprite8x16 {
                // Means we've gotten our 32 BG tiles fetched (32 * 4)
                if self.spr_fetch_count >= 127 && self.spr_fetch_count <= 159 {
                    self.chr_banks_spr[bank_idx]
                } else {
                    self.chr_banks_bg[bank_idx & 0x03]
                }
            } else if self.last_chr_write == ChrBank::Spr {
                self.chr_banks_spr[bank_idx]
            } else {
                self.chr_banks_bg[bank_idx & 0x03]
            };
            bank * bank_size + offset
        }
    }

    // Determine the nametable we're trying to access
    // 0 -> NTA
    // 1 -> NTB
    // 2 -> ExRAM
    // 3 -> Fill-Mode
    fn nametable_mode(&self, addr: u16) -> u16 {
        let table_size = 0x0400;
        let addr = (addr - 0x2000) % 0x1000 as u16;
        let table = addr / table_size;
        u16::from((self.regs.nametable_mirroring >> (2 * table)) & 0x03)
    }

    // Used by the PPU to determine whether it should use it's own internal CIRAM for nametable
    // reads or to read CIRAM instead from the mapper
    fn use_ciram(&self, addr: u16) -> bool {
        // If we're in Extended Attribute mode and reading BG attributes,
        // yield to mapper for Attribute data instead of PPU
        if self.regs.exram_mode == 1
            && (addr % 0x0400) >= 0x3C0
            && (self.spr_fetch_count < 127 || self.spr_fetch_count > 158)
        {
            false
        } else {
            // 0 and 1 mean NametableA and NametableB
            // 2 means internal EXRAM
            // 3 means Fill-mode
            let mode = self.nametable_mode(addr);
            match mode {
                0 | 1 => true,
                _ => false,
            }
        }
    }

    // I'm not sure where this comes from - I referenced it from Nintendulator, but not sure if it's valid
    const ATTRIBUTES: [u8; 4] = [0x00, 0x55, 0xAA, 0xFF];

    // Reads from ExROM memory
    fn read(&mut self, addr: u16) -> u8 {
        match addr {
            0x0000..=0x1FFF => {
                let addr = self.get_chr_addr(addr);
                // readw here allows reading an absolute instead of a 16-bit address
                self.cart.chr_rom.readw(addr)
            }
            0x2000..=0x3EFF => {
                let offset = addr % 0x0400;
                if self.regs.exram_mode == 1 && offset >= 0x03C0 {
                    // Extended Attribute mode should return attributes from exram
                    ATTRIBUTES[(self.exram.read(offset) as usize) >> 6]
                } else if self.regs.exram_mode < 2 {
                    // Otherwise we're using exram as a nametable
                    let mode = self.nametable_mode(addr);
                    // mode 2 means use exram as nametable
                    // mode 3 means use fill-mode nametable
                    match mode {
                        2 => self.exram.read(offset),
                        3 => {
                            // Ensure we return the tile/attr correctly
                            if offset < 0x03C0 {
                                self.regs.fill_tile
                            } else {
                                self.regs.fill_attr
                            }
                        }
                        _ => 0,
                    }
                } else {
                    0
                }
            }
            0x5104 => self.regs.exram_mode,
            0x5105 => self.regs.nametable_mirroring,
            0x5C00..=0x5FFF => {
                // Modes 0-1 are nametable/attr modes and not used for RAM, thus are not readable
                if self.regs.exram_mode < 2 {
                    self.open_bus
                } else {
                    self.exram.read(addr - 0x5C00)
                }
            }
            ... // Other addresses omitted
        }
    }

    // Writes to ExROM memory
    fn write(&mut self, addr: u16, val: u8) {
        match addr {
            0x0000..=0x1FFF => () // CHR ROM is read-only
            0x2000..=0x3EFF => {
                let mode = self.nametable_mode(addr);
                // mode 2 is ExRAM as nametable
                // Ensure exram mode is also set to nametable mode
                if mode == 2 && self.regs.exram_mode < 2 {
                    self.exram.write(addr - 0x2000, val);
                }
            }
            0x5104 => self.regs.exram_mode = val & 0x03,
            0x5105 => {
                self.regs.nametable_mirroring = val;
                self.mirroring = match self.regs.nametable_mirroring {
                    0x50 => Mirroring::Horizontal,
                    0x44 => Mirroring::Vertical,
                    0x00 => Mirroring::SingleScreenA,
                    0x55 => Mirroring::SingleScreenB,
                    // While the below technically isn't true - it forces my implementation to
                    // rely on the Mapper for reading Nametables in any other mode for the missing
                    // two nametables
                    _ => Mirroring::FourScreen,
                };
            }
            0x5C00..=0x5FFF => {
                match self.regs.exram_mode {
                    // Modes 0 and 1 are nametable/extended attribute modes
                    0x00 | 0x01 => {
                        if self.ppu_rendering {
                            self.exram.write(addr - 0x5C00, val);
                        } else {
                            self.exram.write(addr - 0x5C00, 0x00);
                        }
                    }
                    // Mode 2 is use exram as writable RAM
                    0x02 => self.exram.write(addr - 0x5C00, val),
                    _ => () // Not a exram writable mode
                }
            }
            ... // Other writes omitted
        }
    }


Inside the PPU
Code:
    // Maps an address to the approriate nametable based on
    // the mirroring mode
    fn nametable_addr(&self, addr: u16) -> u16 {
        let mirroring = self.mapper.borrow().mirroring();
        // Maps addresses to nametable pages based on mirroring mode
        // by shifting https://wiki.nesdev.com/w/index.php/Mirroring
        let mirroring_shift = match mirroring {
            Mirroring::Horizontal => 11,
            Mirroring::Vertical => 10,
            Mirroring::SingleScreenA => 14,
            Mirroring::SingleScreenB => 13,
            _ => 10,
        };
        let page = (addr >> mirroring_shift) & 1;
        let table_size = 0x0400;
        let offset = addr % table_size;
        NT_START + page * table_size + offset
    }

    // Read from VRAM
    fn read(&mut self, addr: u16) -> u8 {
        match addr {
            0x0000..=0x1FFF => self.mapper.borrow_mut().read(addr),
            0x2000..=0x3EFF => {
                if self.mapper.borrow().use_ciram(addr) {
                    // Use PPU Nametables
                    let mirror_addr = self.nametable_addr(addr);
                    self.nametable.read(mirror_addr % NT_SIZE as u16)
                } else {
                    // Rely on mapper to provide nametable data
                    self.mapper.borrow_mut().read(addr)
                }
            }
            0x3F00..=0x3FFF => self.palette.read(addr % PALETTE_SIZE as u16),
            _ => 0,
        }
    }

    // Write is similar, nothing fancy


Edit: I'm aware that my nametable_addr() implementation above isn't accurate. I thought to modify and simplify it while writing this post without fully testing it. Working on a better implementation (previously I was reading a nametable_addr from the mapper which, if 0, would defer to the nametable_addr() in the PPU, but I'm not a fan of that approach)

Edit 2: Also it seems odd, (and maybe my PRG swapping isn't 100% accurate) that Uncharted Waters attempts to write to CHR ROM on start at address range $000B - $0094


Top
 Profile  
 
 Post subject: Re: MMC5 EXRAM
PostPosted: Mon Nov 04, 2019 11:40 pm 
Offline
User avatar

Joined: Sat Jun 08, 2019 2:53 am
Posts: 22
Location: Portland, OR
Well a slight update.

I managed to get the scrolling intro text to work. I feel sort of silly, my multiplier wasn't working properly.

Some screenshots. BG tiles are still broken, but working on tracking it down.

Attachment:
Screen Shot 2019-11-04 at 22.35.49.png
Screen Shot 2019-11-04 at 22.35.49.png [ 24.52 KiB | Viewed 1151 times ]


Attachment:
Screen Shot 2019-11-04 at 22.39.09.png
Screen Shot 2019-11-04 at 22.39.09.png [ 26.84 KiB | Viewed 1151 times ]


Top
 Profile  
 
 Post subject: Re: MMC5 EXRAM
PostPosted: Tue Nov 05, 2019 1:42 am 
Offline
User avatar

Joined: Sat Jun 08, 2019 2:53 am
Posts: 22
Location: Portland, OR
I figured it out finally!

I was missing a key bit from natt on this post:
Quote:
1. PPU fetches an NT byte; that is, (addr >= 0x2000 && (addr & 0x3ff) < 0x3c0)
Return the NT fetched byte as requested from whatever nametable is in context. Let N = addr & 0x3ff; that will be your index into ExRAM for the next fetch.


The way my code is structured did not lend itself to noticing this since the mapper is so isolated from each PPU cycle. I was attempting to use the current address from the low tile byte being read which obviously was wrong and didn't map properly to the actual NT byte in EXRAM.

Hope this helps someone else! I didn't find really any of natt's steps in the wiki anywhere. Having his step-by-step in the wiki under Ex mode 1 would be useful.


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

All times are UTC - 7 hours


Who is online

Users browsing this forum: infidelity, MSN [Bot] and 5 guests


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