Game Boy Color PPU emulation [solved]

Discussion of programming and development for the original Game Boy and Game Boy Color.
Post Reply
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Game Boy Color PPU emulation [solved]

Post by Near »

So, I've tried following pandocs to the best of my ability, but graphics seem completely broken on the Zelda Oracle games.

Image Image Image

I'm going to keep digging at it myself, but if anyone here is familiar with how GBC rendering works, could you please take a look at my emulation below and point out any potential issues? Would greatly appreciate any help!

(Possible it's not a PPU bug, of course. But it seems the logical place to start.)

Code: Select all

void PPU::cgb_render() {
  for(auto& pixel : pixels) {
    pixel.color = 0x7fff;
    pixel.palette = 0;
    pixel.origin = Pixel::Origin::None;
  }

  if(status.display_enable) {
    cgb_render_bg();
    if(status.window_display_enable) cgb_render_window();
    if(status.ob_enable) cgb_render_ob();
  }

  uint32* output = screen + status.ly * 160;
  for(unsigned n = 0; n < 160; n++) output[n] = video.palette[pixels[n].color];
  interface->lcdScanline();
}

//Attributes:
//0x80: 0 = OAM priority, 1 = BG priority
//0x40: vertical flip
//0x20: horizontal flip
//0x08: VRAM bank#
//0x07: palette#
void PPU::cgb_read_tile(bool select, unsigned x, unsigned y, unsigned& tile, unsigned& attr, unsigned& data) {
  unsigned tmaddr = 0x1800 + (select << 10);
  tmaddr += (((y >> 3) << 5) + (x >> 3)) & 0x03ff;

  tile = vram[0x0000 + tmaddr];
  attr = vram[0x2000 + tmaddr];

  unsigned tdaddr = attr & 0x08 ? 0x2000 : 0x0000;
  if(status.bg_tiledata_select == 0) {
    tdaddr += 0x1000 + ((int8)tile << 4);
  } else {
    tdaddr += 0x0000 + (tile << 4);
  }

  y &= 7;
  if(attr & 0x40) y ^= 7;
  tdaddr += y << 1;

  data  = vram[tdaddr++] << 0;
  data |= vram[tdaddr++] << 8;
  if(attr & 0x20) data = hflip(data);
}

void PPU::cgb_render_bg() {
  unsigned iy = (status.ly + status.scy) & 255;
  unsigned ix = status.scx, tx = ix & 7;

  unsigned tile, attr, data;
  cgb_read_tile(status.bg_tilemap_select, ix, iy, tile, attr, data);

  for(unsigned ox = 0; ox < 160; ox++) {
    unsigned index = ((data & (0x0080 >> tx)) ? 1 : 0)
                   | ((data & (0x8000 >> tx)) ? 2 : 0);
    unsigned palette = ((attr & 0x07) << 2) + index;
    unsigned color = 0;
    color |= bgpd[(palette << 1) + 0] << 0;
    color |= bgpd[(palette << 1) + 1] << 8;
    color &= 0x7fff;

    pixels[ox].color = color;
    pixels[ox].palette = index;
    pixels[ox].origin = (attr & 0x80 ? Pixel::Origin::BGP : Pixel::Origin::BG);

    ix = (ix + 1) & 255;
    tx = (tx + 1) & 7;
    if(tx == 0) cgb_read_tile(status.bg_tilemap_select, ix, iy, tile, attr, data);
  }
}

void PPU::cgb_render_window() {
  if(status.ly - status.wy >= 144u) return;
  if(status.wx >= 167u) return;
  unsigned iy = status.wyc++;
  unsigned ix = (7 - status.wx) & 255, tx = ix & 7;

  unsigned tile, attr, data;
  cgb_read_tile(status.window_tilemap_select, ix, iy, tile, attr, data);

  for(unsigned ox = 0; ox < 160; ox++) {
    unsigned index = ((data & (0x0080 >> tx)) ? 1 : 0)
                   | ((data & (0x8000 >> tx)) ? 2 : 0);
    unsigned palette = ((attr & 0x07) << 2) + index;
    unsigned color = 0;
    color |= bgpd[(palette << 1) + 0] << 0;
    color |= bgpd[(palette << 1) + 1] << 8;
    color &= 0x7fff;

    if(ox - (status.wx - 7) < 160u) {
      pixels[ox].color = color;
      pixels[ox].palette = index;
      pixels[ox].origin = (attr & 0x80 ? Pixel::Origin::BGP : Pixel::Origin::BG);
    }

    ix = (ix + 1) & 255;
    tx = (tx + 1) & 7;
    if(tx == 0) cgb_read_tile(status.window_tilemap_select, ix, iy, tile, attr, data);
  }
}

//Attributes:
//0x80: 0 = OBJ above BG, 1 = BG above OBJ
//0x40: vertical flip
//0x20: horizontal flip
//0x08: VRAM bank#
//0x07: palette#
void PPU::cgb_render_ob() {
  const unsigned Height = (status.ob_size == 0 ? 8 : 16);
  unsigned sprite[10], sprites = 0;

  //find first ten sprites on this scanline
  for(unsigned s = 0; s < 40; s++) {
    unsigned sy = oam[(s << 2) + 0] - 16;
    unsigned sx = oam[(s << 2) + 1] -  8;

    sy = status.ly - sy;
    if(sy >= Height) continue;

    sprite[sprites++] = s;
    if(sprites == 10) break;
  }

  //sort by X-coordinate, when equal, lower address comes first
  for(unsigned x = 0; x < sprites; x++) {
    for(unsigned y = x + 1; y < sprites; y++) {
      signed sx = oam[(sprite[x] << 2) + 1] - 8;
      signed sy = oam[(sprite[y] << 2) + 1] - 8;
      if(sy < sx) {
        sprite[x] ^= sprite[y];
        sprite[y] ^= sprite[x];
        sprite[x] ^= sprite[y];
      }
    }
  }

  //render backwards, so that first sprite has highest priority
  for(signed s = sprites - 1; s >= 0; s--) {
    unsigned n = sprite[s] << 2;
    unsigned sy = oam[n + 0] - 16;
    unsigned sx = oam[n + 1] -  8;
    unsigned tile = oam[n + 2] & ~status.ob_size;
    unsigned attr = oam[n + 3];

    sy = status.ly - sy;
    if(sy >= Height) continue;
    if(attr & 0x40) sy ^= (Height - 1);

    unsigned tdaddr = (attr & 0x08 ? 0x2000 : 0x0000) + (tile << 4) + (sy << 1), data = 0;
    data |= vram[tdaddr++] << 0;
    data |= vram[tdaddr++] << 8;
    if(attr & 0x20) data = hflip(data);

    for(unsigned tx = 0; tx < 8; tx++) {
      unsigned index = ((data & (0x0080 >> tx)) ? 1 : 0)
                     | ((data & (0x8000 >> tx)) ? 2 : 0);
      if(index == 0) continue;

      unsigned palette = ((attr & 0x07) << 2) + index;
      unsigned color = 0;
      color |= obpd[(palette << 1) + 0] << 0;
      color |= obpd[(palette << 1) + 1] << 8;
      color &= 0x7fff;

      unsigned ox = sx + tx;
      if(ox < 160) {
        //When LCDC.D0 (BG enable) is off, OB is always rendered above BG+Window
        if(status.bg_enable) {
          if(pixels[ox].origin == Pixel::Origin::BGP) continue;
          if(attr & 0x80) {
            if(pixels[ox].origin == Pixel::Origin::BG) {
              if(pixels[ox].palette > 0) continue;
            }
          }
        }
        pixels[ox].color = color;
        pixels[ox].palette = index;
        pixels[ox].origin = Pixel::Origin::OB;
      }
    }
  }
}
Last edited by Near on Tue Dec 10, 2013 10:51 am, edited 1 time in total.
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Game Boy Color PPU emulation

Post by nitro2k01 »

This looks like an off-by-one error in reading the GBC's secondary VRAM map, ie the attribute map. The attribute for each tile is read taken from the previous tile. Another possible, less obvious source for this error is the GBC VRAM DMA transfer, which Oo* is using to transfer data to VRAM.
Near
Founder of higan project
Posts: 1553
Joined: Mon Mar 27, 2006 5:23 pm

Re: Game Boy Color PPU emulation

Post by Near »

Oh wow, you are really good. Thank you so very much.

Image

> This looks like an off-by-one error in reading the GBC's secondary VRAM map, ie the attribute map. The attribute for each tile is read taken from the previous tile.

Really? My code doesn't do this at all.

Code: Select all

void PPU::cgb_read_tile(bool select, unsigned x, unsigned y, unsigned& tile, unsigned& attr, unsigned& data) {
  unsigned tmaddr = 0x1800 + (select << 10);
  tmaddr += (((y >> 3) << 5) + (x >> 3)) & 0x03ff;

  tile = vram[0x0000 + tmaddr];
  attr = vram[0x2000 + tmaddr];

  unsigned tdaddr = attr & 0x08 ? 0x2000 : 0x0000;
  if(status.bg_tiledata_select == 0) {
    tdaddr += 0x1000 + ((int8)tile << 4);
  } else {
    tdaddr += 0x0000 + (tile << 4);
  }

  y &= 7;
  if(attr & 0x40) y ^= 7;
  tdaddr += y << 1;

  data  = vram[tdaddr++] << 0;
  data |= vram[tdaddr++] << 8;
  if(attr & 0x20) data = hflip(data);
}
If I edit attr to read from vram[0x2000 + tmaddr - 1], all of the graphics end up corrupted. Whereas everything works as it is now.

> Another possible, less obvious source for this error is the GBC VRAM DMA transfer, which Oo* is using to transfer data to VRAM.

You nailed it.

I wasn't masking the low 4-bits of the HDMA12 (source), HDMA34 (target) addresses. Apparently this game is setting them to 1-15, and expecting it to act as 0. Fixing that results in the game working correctly.

Thanks so much! You saved me a lot of time pointing me in the exact right area. I've never played these games before, so it'll be fun giving them a try now!
nitro2k01
Posts: 252
Joined: Sat Aug 28, 2010 9:01 am

Re: Game Boy Color PPU emulation

Post by nitro2k01 »

byuu wrote:If I edit attr to read from vram[0x2000 + tmaddr - 1], all of the graphics end up corrupted. Whereas everything works as it is now.
Not that it really matters, but that would be +1 not -1, if you wanted to fix this locally/stupidly in the Oo* games.
Post Reply