Mappers and PRG pages

This is an archive of a topic from NESdev BBS, taken in mid-October 2019 before a server upgrade.
View original topic
Mappers and PRG pages
by on (#23815)
Hello.

I'm starting out in NES emulation, and have quite a few (probably stupid) questions.
I've read a lot of documentation, and have a working CPU core and ROM loader, but mappers are confusing me.

I've read some posts about keeping pointers for PRG "pages", but I don't think I really know what I'm doing!
For the minute, I'm only catering for NROM carts, but I'd like to make my code easily "upgradeable" to handle other mappers too. Would this be right for an NROM cart, assuming prg_rom is loaded with the cart's ROM data (as per the iNES header)?

Code:
byte *prg_rom;
byte *prg_page[10];      // 4k pages from $6000
...
void nrom_init()
{
   int count, num;

   for(count = 0, num = 0; count <= 0x10000; count += 0x1000, num++)
      prg_page[num] = &prg_rom[count];

   if(prg_pages == 1)
   {   
      prg_page[2] = prg_rom;   // $8000
      prg_page[6] = prg_rom;   // $C000
   }
}


... or am I completely messing that up?

Thanks for any help/suggestions.

by on (#23824)
You have the right idea.

I do pretty much the exact same thing. I keep 16 pointers for PRG (one for each 4k page -- I know not all 4k pages can be PRG, but having 16 pointers makes the math a little simpler) then simpy use those pointers to index PRG. When mappers swap PRG, all you'll have to do is change where those pointers point.

something like the following:

Code:
u8* prg[0x10];    //  pointers to PRG pages
u8* prg_buf;    //  the actual buffer containing the PRG


u8 ReadPRG( u16 address )
{
  return prg[address >> 12][address & 0x0FFF];
}

void SwapPRG_16k( int where, int page )
{
  page *= 0x4000;  // multiply page index by 16k
  prg[where+0] = &prg_buf[ page + 0x0000 ];
  prg[where+1] = &prg_buf[ page + 0x1000 ];
  prg[where+2] = &prg_buf[ page + 0x2000 ];
  prg[where+3] = &prg_buf[ page + 0x3000 ];
}

void LoadROM()
{
  // ...
 
  // for NROM:

  if( ROM_IS_16k )
  {
    SwapPRG_16k( 0x8, 0 );  // 16k page 0 goes at $8000
    SwapPRG_16k( 0xC, 0 );  // and at $C000
  }
  else  // ROM is 32k
  {
     SwapPRG_16k( 0x8, 0 );
     SwapPRG_16k( 0xC, 1 );
  }
}



That's simplified a bit. You'll probably want to do some kind of bounds checking or wrapping when you swap PRG in so that you don't go to a page that doesn't exist.

I also have SwapPRG_4k, 8k, and 32k functions -- all of which work the same way, but in smaller pages.


You probably will want to do the same thing with 1k CHR pages -- as mappers swap CHR as well as PRG. And if you REALLY want to accomidate for future games... have options to swap in ROM or RAM. Some mappers have both CHR-ROM and CHR-RAM... and some others (MMC5 comes to mind) can swap PRG-ROM or PRG-RAM (SRAM) into the $8000-DFFF range.

by on (#23853)
Thanks Disch, that's really helped to sort it out in my mind :)

I'm sure I'll be back with more questions soon!

by on (#23855)
[Split from this post]

As additional note, you could use shifting instead of multiplying numbers. No clue, but looks faster..?

by on (#23856)
If your emulator is written in C, then compilers should turn multiplies by a constant power of 2 into shifts.

by on (#23868)
tepples wrote:
If your emulator is written in C, then compilers should turn multiplies by a constant power of 2 into shifts.


Should. But it's bad programming to rely on a compiler to do the work for you.

by on (#23873)
Quote:
But it's bad programming to rely on a compiler to do the work for you.

That's exactly what the compiler's job is. Your job as a programmer is to make use of your tools so that your code is as clear as possible.

by on (#23874)
blargg wrote:
Quote:
But it's bad programming to rely on a compiler to do the work for you.

That's exactly what the compiler's job is. Your job as a programmer is to make use of your tools so that your code is as clear as possible.


No, that would be to turn the text into a binary file. It's still bad programming to rely on a compiler to do the work for you.

by on (#23875)
WedNESday wrote:
blargg wrote:
Quote:
But it's bad programming to rely on a compiler to do the work for you.

That's exactly what the compiler's job is. Your job as a programmer is to make use of your tools so that your code is as clear as possible.

No, that would be to turn the text into a binary file.

A compiler's job is to turn your code into the best binary file that implements the semantics of your code. Depending on the options you specify, "best" can mean "most quickly produced" (e.g. gcc -O0), "smallest" (e.g. gcc -Os), or "fastest executing" (e.g. gcc -O3).

Quote:
It's still bad programming to rely on a compiler to do the work for you.

Then why not program Windows-based tools directly in assembly language?

by on (#23884)
WedNESday wrote:
Should. But it's bad programming to rely on a compiler to do the work for you.

If your compiler is incapable of this most very basic form of strength reduction, you have much bigger problems than can be fixed by converting multiplies and divides into shifts.
And then what happens when your new computer's architecture does integer multiplication faster than bit shifting? That's an simplified example, but this stuff happens all the time.
Here's a prime example of Knuth's infamous root of all evil.

by on (#23887)
(I suggest to split this topic starting on my -first- message...)

- Yes, I got the spirit. By the way, it's not the most smart example of programming vs compiler. You can think of 2^n as 'golden' values that can be written using shifts. However, life isn't measured as 2^n numbers, and potentially we're deep into a C code written badly, unoptimized and only the coder knows the meaning of that 'encrypted code'.

- Anyway, personally, I never did a binary comparision between two codes. You know that a compiler has bugs. Some of you can detect obscure annoyances... I can't. Probably you know how to write a piece of C code into ASM (dynamic recompilation, on CPU emulation): I don't. Will the compiler do the task? I won't. ;)

by on (#23894)
But it's STILL bad programming to rely on the compiler to do the work for you.

by on (#23896)
I agree that * 0x1000 vs << 12 is easier to read, and that is absolutely fine. I did go to that link, and it does make some good points about how * 0x2000 could be better than a <<. But the point I'm trying to make is, not just about shifts vs multiplying, it's about expecting someone/something else to do the work for you. Because I've only even needed to write it once in my emulator, if I lost my directx.cpp file, I would have to go back online or get a directx book because I've forgotten how to do some of the specifics. Same goes for the letting someone/something else do something for you too much, you end up forgetting how to do it properly.

by on (#23897)
Hey Disch deleted that post! That aint fair! :!:

by on (#23898)
Yeah I deleted my post shortly after posting it. I was hoping to do it before anyone saw it (and before anyone replied to it)

I decided this wasn't a discussion I wanted to pursue

by on (#23899)
A compiler's primary job is to make the binary, it's secondary job is to make its fast or whatever.

Plus some code might be for an exam and checked by an examiner and not to be compiled, and that might mean points lost.

by on (#23912)
WedNESday, it's NOT "bad programming", since the compiler WILL DO the things. I know it's sound as YOUR (or OUR) obligation to optimize that thing. The example posted relies in ANOTHER fact: of your code being "unreadable" and very probably unoptimized. Shifts are a detail, as far as I'm concerned.

However, I see as "bad programming" (as example, nothing directly implied, please!) an emulator source that makes the compiler to warn A LOT about stupidities that relies on programmer's lazyness... ;)

by on (#23943)
WedNESday, give a few examples of your claim as it applies to writing code (not using libraries), otherwise, can it. As in, if you write the following code [...] then on such-and-such compiler, it won't work as you expected.

by on (#23975)
Umm... I have an example:

Code:
int i = 0;
while(i < 0x100)
{
    value = readbyte(address); address++
    write(0x2004,value);
    value = readbyte(address); address++
    write(0x2004,value); i+=2;
}

by on (#23978)
Fx3 wrote:
Umm... I have an example:

Code:
int i = 0;
while(i < 0x100)
{
    value = readbyte(address); address++
    write(0x2004,value);
    value = readbyte(address); address++
    write(0x2004,value); i+=2;
}
What's your point with that code?

by on (#23983)
It should be...

Code:
int i = 0;
while(i < 0x100)
{
    value = readbyte(address); address++
    write(0x2004,value); i++;
}


- Notice that repeating the block (twice) and increasing i by 2 "might" make the things faster. No clues, I saw that "trick" somewhere... ;) What does the compiler when -funroll-loops is used?