auto PPU::latchCounters() -> void {
  cpu.synchronize(ppu);
  io.hcounter = hdot();
  io.vcounter = vcounter();
  latch.counters = 1;
}

alwaysinline auto PPU::addressVRAM() const -> uint16 {
  uint16 address = io.vramAddress;
  switch(io.vramMapping) {
  case 0: return address;
  case 1: return address.bit( 8,15) <<  8 | address.bit(0,4) << 3 | address.bit(5,7);
  case 2: return address.bit( 9,15) <<  9 | address.bit(0,5) << 3 | address.bit(6,8);
  case 3: return address.bit(10,15) << 10 | address.bit(0,6) << 3 | address.bit(7,9);
  }
  unreachable;
}

alwaysinline auto PPU::readVRAM() -> uint16 {
  if(!io.displayDisable && vcounter() < vdisp()) return 0x0000;
  auto address = addressVRAM();
  return vram[address];
}

alwaysinline auto PPU::writeVRAM(uint1 byte, uint8 data) -> void {
  if(!io.displayDisable && vcounter() < vdisp()) return;
  auto address = addressVRAM();
  vram[address].byte(byte) = data;
}

alwaysinline auto PPU::readOAM(uint10 address) -> uint8 {
  if(!io.displayDisable && vcounter() < vdisp()) {
    if(address.bit(9) == 0) return obj.oam.read(0x000 | latch.oamAddress << 2 | address & 1);
    if(address.bit(9) == 1) return obj.oam.read(0x200 | latch.oamAddress >> 2);
  }
  return obj.oam.read(address);
}

alwaysinline auto PPU::writeOAM(uint10 address, uint8 data) -> void {
  if(!io.displayDisable && vcounter() < vdisp()) {
    if(address.bit(9) == 0) return obj.oam.write(0x000 | latch.oamAddress << 2 | address & 1, data);
    if(address.bit(9) == 1) return obj.oam.write(0x200 | latch.oamAddress >> 2, data);
  }
  return obj.oam.write(address, data);
}

alwaysinline auto PPU::readCGRAM(uint1 byte, uint8 address) -> uint8 {
  if(!io.displayDisable
  && vcounter() > 0 && vcounter() < vdisp()
  && hcounter() >= 88 && hcounter() < 1096
  ) address = latch.cgramAddress;
  return dac.cgram[address].byte(byte);
}

alwaysinline auto PPU::writeCGRAM(uint8 address, uint15 data) -> void {
  if(!io.displayDisable
  && vcounter() > 0 && vcounter() < vdisp()
  && hcounter() >= 88 && hcounter() < 1096
  ) address = latch.cgramAddress;
  dac.cgram[address] = data;
}

auto PPU::readIO(uint24 address, uint8 data) -> uint8 {
  cpu.synchronize(ppu);

  switch((uint16)address) {

  case 0x2104: case 0x2105: case 0x2106: case 0x2108:
  case 0x2109: case 0x210a: case 0x2114: case 0x2115:
  case 0x2116: case 0x2118: case 0x2119: case 0x211a:
  case 0x2124: case 0x2125: case 0x2126: case 0x2128:
  case 0x2129: case 0x212a: {
    return ppu1.mdr;
  }

  //MPYL
  case 0x2134: {
    uint24 result = (int16)io.m7a * (int8)(io.m7b >> 8);
    return ppu1.mdr = result.byte(0);
  }

  //MPYM
  case 0x2135: {
    uint24 result = (int16)io.m7a * (int8)(io.m7b >> 8);
    return ppu1.mdr = result.byte(1);
  }

  //MPYH
  case 0x2136: {
    uint24 result = (int16)io.m7a * (int8)(io.m7b >> 8);
    return ppu1.mdr = result.byte(2);
  }

  //SLHV
  case 0x2137: {
    if(cpu.pio().bit(7)) latchCounters();
    return data;  //CPU MDR
  }

  //OAMDATAREAD
  case 0x2138: {
    ppu1.mdr = readOAM(io.oamAddress++);
    obj.setFirstSprite();
    return ppu1.mdr;
  }

  //VMDATALREAD
  case 0x2139: {
    ppu1.mdr = latch.vram.byte(0);
    if(io.vramIncrementMode == 0) {
      latch.vram = readVRAM();
      io.vramAddress += io.vramIncrementSize;
    }
    return ppu1.mdr;
  }

  //VMDATAHREAD
  case 0x213a: {
    ppu1.mdr = latch.vram.byte(1);
    if(io.vramIncrementMode == 1) {
      latch.vram = readVRAM();
      io.vramAddress += io.vramIncrementSize;
    }
    return ppu1.mdr;
  }

  //CGDATAREAD
  case 0x213b: {
    if(io.cgramAddressLatch++ == 0) {
      ppu2.mdr.bit(0,7) = readCGRAM(0, io.cgramAddress);
    } else {
      ppu2.mdr.bit(0,6) = readCGRAM(1, io.cgramAddress++);
    }
    return ppu2.mdr;
  }

  //OPHCT
  case 0x213c: {
    if(latch.hcounter++ == 0) {
      ppu2.mdr.bit(0,7) = io.hcounter.bit(0,7);
    } else {
      ppu2.mdr.bit(0)   = io.hcounter.bit(8);
    }
    return ppu2.mdr;
  }

  //OPVCT
  case 0x213d: {
    if(latch.vcounter++ == 0) {
      ppu2.mdr.bit(0,7) = io.vcounter.bit(0,7);
    } else {
      ppu2.mdr.bit(0)   = io.vcounter.bit(8);
    }
    return ppu2.mdr;
  }

  //STAT77
  case 0x213e: {
    ppu1.mdr.bit(0,3) = ppu1.version;
    ppu1.mdr.bit(5)   = 0;
    ppu1.mdr.bit(6)   = obj.io.rangeOver;
    ppu1.mdr.bit(7)   = obj.io.timeOver;
    return ppu1.mdr;
  }

  //STAT78
  case 0x213f: {
    latch.hcounter = 0;
    latch.vcounter = 0;
    ppu2.mdr.bit(0,3) = ppu2.version;
    ppu2.mdr.bit(4)   = Region::PAL();  //0 = NTSC, 1 = PAL
    if(!cpu.pio().bit(7)) {
      ppu2.mdr.bit(6) = 1;
    } else {
      ppu2.mdr.bit(6) = latch.counters;
      latch.counters = 0;
    }
    ppu2.mdr.bit(7) = field();
    return ppu2.mdr;
  }

  }

  return data;
}

auto PPU::writeIO(uint24 address, uint8 data) -> void {
  cpu.synchronize(ppu);

  switch((uint16)address) {

  //INIDISP
  case 0x2100: {
    if(io.displayDisable && vcounter() == vdisp()) obj.addressReset();
    io.displayBrightness = data.bit(0,3);
    io.displayDisable    = data.bit(7);
    return;
  }

  //OBSEL
  case 0x2101: {
    obj.io.tiledataAddress = data.bit(0,2) << 13;
    obj.io.nameselect      = data.bit(3,4);
    obj.io.baseSize        = data.bit(5,7);
    return;
  }

  //OAMADDL
  case 0x2102: {
    io.oamBaseAddress.bit(1,8) = data.bit(0,7);
    obj.addressReset();
    return;
  }

  //OAMADDH
  case 0x2103: {
    io.oamBaseAddress.bit(9) = data.bit(0);
    io.oamPriority = data.bit(7);
    obj.addressReset();
    return;
  }

  //OAMDATA
  case 0x2104: {
    uint1 latchBit = io.oamAddress.bit(0);
    uint10 address = io.oamAddress++;
    if(latchBit == 0) latch.oam = data;
    if(address.bit(9)) {
      writeOAM(address, data);
    } else if(latchBit == 1) {
      writeOAM((address & ~1) + 0, latch.oam);
      writeOAM((address & ~1) + 1, data);
    }
    obj.setFirstSprite();
    return;
  }

  //BGMODE
  case 0x2105: {
    io.bgMode       = data.bit(0,2);
    io.bgPriority   = data.bit(3);
    bg1.io.tileSize = data.bit(4);
    bg2.io.tileSize = data.bit(5);
    bg3.io.tileSize = data.bit(6);
    bg4.io.tileSize = data.bit(7);
    updateVideoMode();
    return;
  }

  //MOSAIC
  case 0x2106: {
    bool mosaicEnable = mosaic.enable();
    bg1.mosaic.enable = data.bit(0);
    bg2.mosaic.enable = data.bit(1);
    bg3.mosaic.enable = data.bit(2);
    bg4.mosaic.enable = data.bit(3);
    mosaic.size       = data.bit(4,7) + 1;
    if(!mosaicEnable && mosaic.enable()) {
      //mosaic vcounter is reloaded when mosaic becomes enabled
      mosaic.vcounter = mosaic.size + 1;
    }
    return;
  }

  //BG1SC
  case 0x2107: {
    bg1.io.screenSize    = data.bit(0,1);
    bg1.io.screenAddress = data.bit(2,7) << 10;
    return;
  }

  //BG2SC
  case 0x2108: {
    bg2.io.screenSize    = data.bit(0,1);
    bg2.io.screenAddress = data.bit(2,7) << 10;
    return;
  }

  //BG3SC
  case 0x2109: {
    bg3.io.screenSize    = data.bit(0,1);
    bg3.io.screenAddress = data.bit(2,7) << 10;
    return;
  }

  //BG4SC
  case 0x210a: {
    bg4.io.screenSize    = data.bit(0,1);
    bg4.io.screenAddress = data.bit(2,7) << 10;
    return;
  }

  //BG12NBA
  case 0x210b: {
    bg1.io.tiledataAddress = data.bit(0,3) << 12;
    bg2.io.tiledataAddress = data.bit(4,7) << 12;
    return;
  }

  //BG34NBA
  case 0x210c: {
    bg3.io.tiledataAddress = data.bit(0,3) << 12;
    bg4.io.tiledataAddress = data.bit(4,7) << 12;
    return;
  }

  //BG1HOFS
  case 0x210d: {
    io.hoffsetMode7 = data << 8 | latch.mode7;
    latch.mode7 = data;

    bg1.io.hoffset = data << 8 | latch.bgofsPPU1 & ~7 | latch.bgofsPPU2 & 7;
    latch.bgofsPPU1 = data;
    latch.bgofsPPU2 = data;
    return;
  }

  //BG1VOFS
  case 0x210e: {
    io.voffsetMode7 = data << 8 | latch.mode7;
    latch.mode7 = data;

    bg1.io.voffset = data << 8 | latch.bgofsPPU1;
    latch.bgofsPPU1 = data;
    return;
  }

  //BG2HOFS
  case 0x210f: {
    bg2.io.hoffset = data << 8 | latch.bgofsPPU1 & ~7 | latch.bgofsPPU2 & 7;
    latch.bgofsPPU1 = data;
    latch.bgofsPPU2 = data;
    return;
  }

  //BG2VOFS
  case 0x2110: {
    bg2.io.voffset = data << 8 | latch.bgofsPPU1;
    latch.bgofsPPU1 = data;
    return;
  }

  //BG3HOFS
  case 0x2111: {
    bg3.io.hoffset = data << 8 | latch.bgofsPPU1 & ~7 | latch.bgofsPPU2 & 7;
    latch.bgofsPPU1 = data;
    latch.bgofsPPU2 = data;
    return;
  }

  //BG3VOFS
  case 0x2112: {
    bg3.io.voffset = data << 8 | latch.bgofsPPU1;
    latch.bgofsPPU1 = data;
    return;
  }

  //BG4HOFS
  case 0x2113: {
    bg4.io.hoffset = data << 8 | latch.bgofsPPU1 & ~7 | latch.bgofsPPU2 & 7;
    latch.bgofsPPU1 = data;
    latch.bgofsPPU2 = data;
    return;
  }

  //BG4VOFS
  case 0x2114: {
    bg4.io.voffset = data << 8 | latch.bgofsPPU1;
    latch.bgofsPPU1 = data;
    return;
  }

  //VMAIN
  case 0x2115: {
    static const uint size[4] = {1, 32, 128, 128};
    io.vramIncrementSize = size[data.bit(0,1)];
    io.vramMapping       = data.bit(2,3);
    io.vramIncrementMode = data.bit(7);
    return;
  }

  //VMADDL
  case 0x2116: {
    io.vramAddress.bit(0,7) = data;
    latch.vram = readVRAM();
    return;
  }

  //VMADDH
  case 0x2117: {
    io.vramAddress.bit(8,15) = data;
    latch.vram = readVRAM();
    return;
  }

  //VMDATAL
  case 0x2118: {
    writeVRAM(0, data);
    if(io.vramIncrementMode == 0) io.vramAddress += io.vramIncrementSize;
    return;
  }

  //VMDATAH
  case 0x2119: {
    writeVRAM(1, data);
    if(io.vramIncrementMode == 1) io.vramAddress += io.vramIncrementSize;
    return;
  }

  //M7SEL
  case 0x211a: {
    io.hflipMode7  = data.bit(0);
    io.vflipMode7  = data.bit(1);
    io.repeatMode7 = data.bit(6,7);
    return;
  }

  //M7A
  case 0x211b: {
    io.m7a = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //M7B
  case 0x211c: {
    io.m7b = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //M7C
  case 0x211d: {
    io.m7c = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //M7D
  case 0x211e: {
    io.m7d = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //M7X
  case 0x211f: {
    io.m7x = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //M7Y
  case 0x2120: {
    io.m7y = data << 8 | latch.mode7;
    latch.mode7 = data;
    return;
  }

  //CGADD
  case 0x2121: {
    io.cgramAddress = data;
    io.cgramAddressLatch = 0;
    return;
  }

  //CGDATA
  case 0x2122: {
    if(io.cgramAddressLatch++ == 0) {
      latch.cgram = data;
    } else {
      writeCGRAM(io.cgramAddress++, data.bit(0,6) << 8 | latch.cgram);
    }
    return;
  }

  //W12SEL
  case 0x2123: {
    window.io.bg1.oneInvert = data.bit(0);
    window.io.bg1.oneEnable = data.bit(1);
    window.io.bg1.twoInvert = data.bit(2);
    window.io.bg1.twoEnable = data.bit(3);
    window.io.bg2.oneInvert = data.bit(4);
    window.io.bg2.oneEnable = data.bit(5);
    window.io.bg2.twoInvert = data.bit(6);
    window.io.bg2.twoEnable = data.bit(7);
    return;
  }

  //W34SEL
  case 0x2124: {
    window.io.bg3.oneInvert = data.bit(0);
    window.io.bg3.oneEnable = data.bit(1);
    window.io.bg3.twoInvert = data.bit(2);
    window.io.bg3.twoEnable = data.bit(3);
    window.io.bg4.oneInvert = data.bit(4);
    window.io.bg4.oneEnable = data.bit(5);
    window.io.bg4.twoInvert = data.bit(6);
    window.io.bg4.twoEnable = data.bit(7);
    return;
  }

  //WOBJSEL
  case 0x2125: {
    window.io.obj.oneInvert = data.bit(0);
    window.io.obj.oneEnable = data.bit(1);
    window.io.obj.twoInvert = data.bit(2);
    window.io.obj.twoEnable = data.bit(3);
    window.io.col.oneInvert = data.bit(4);
    window.io.col.oneEnable = data.bit(5);
    window.io.col.twoInvert = data.bit(6);
    window.io.col.twoEnable = data.bit(7);
    return;
  }

  //WH0
  case 0x2126: {
    window.io.oneLeft = data;
    return;
  }

  //WH1
  case 0x2127: {
    window.io.oneRight = data;
    return;
  }

  //WH2
  case 0x2128: {
    window.io.twoLeft = data;
    return;
  }

  //WH3
  case 0x2129: {
    window.io.twoRight = data;
    return;
  }

  //WBGLOG
  case 0x212a: {
    window.io.bg1.mask = data.bit(0,1);
    window.io.bg2.mask = data.bit(2,3);
    window.io.bg3.mask = data.bit(4,5);
    window.io.bg4.mask = data.bit(6,7);
    return;
  }

  //WOBJLOG
  case 0x212b: {
    window.io.obj.mask = data.bit(0,1);
    window.io.col.mask = data.bit(2,3);
    return;
  }

  //TM
  case 0x212c: {
    bg1.io.aboveEnable = data.bit(0);
    bg2.io.aboveEnable = data.bit(1);
    bg3.io.aboveEnable = data.bit(2);
    bg4.io.aboveEnable = data.bit(3);
    obj.io.aboveEnable = data.bit(4);
    return;
  }

  //TS
  case 0x212d: {
    bg1.io.belowEnable = data.bit(0);
    bg2.io.belowEnable = data.bit(1);
    bg3.io.belowEnable = data.bit(2);
    bg4.io.belowEnable = data.bit(3);
    obj.io.belowEnable = data.bit(4);
    return;
  }

  //TMW
  case 0x212e: {
    window.io.bg1.aboveEnable = data.bit(0);
    window.io.bg2.aboveEnable = data.bit(1);
    window.io.bg3.aboveEnable = data.bit(2);
    window.io.bg4.aboveEnable = data.bit(3);
    window.io.obj.aboveEnable = data.bit(4);
    return;
  }

  //TSW
  case 0x212f: {
    window.io.bg1.belowEnable = data.bit(0);
    window.io.bg2.belowEnable = data.bit(1);
    window.io.bg3.belowEnable = data.bit(2);
    window.io.bg4.belowEnable = data.bit(3);
    window.io.obj.belowEnable = data.bit(4);
    return;
  }

  //CGWSEL
  case 0x2130: {
    dac.io.directColor      = data.bit(0);
    dac.io.blendMode        = data.bit(1);
    window.io.col.belowMask = data.bit(4,5);
    window.io.col.aboveMask = data.bit(6,7);
    return;
  }

  //CGADDSUB
  case 0x2131: {
    dac.io.bg1.colorEnable  = data.bit(0);
    dac.io.bg2.colorEnable  = data.bit(1);
    dac.io.bg3.colorEnable  = data.bit(2);
    dac.io.bg4.colorEnable  = data.bit(3);
    dac.io.obj.colorEnable  = data.bit(4);
    dac.io.back.colorEnable = data.bit(5);
    dac.io.colorHalve       = data.bit(6);
    dac.io.colorMode        = data.bit(7);
    return;
  }

  //COLDATA
  case 0x2132: {
    if(data.bit(5)) dac.io.colorRed   = data.bit(0,4);
    if(data.bit(6)) dac.io.colorGreen = data.bit(0,4);
    if(data.bit(7)) dac.io.colorBlue  = data.bit(0,4);
    return;
  }

  //SETINI
  case 0x2133: {
    io.interlace     = data.bit(0);
    obj.io.interlace = data.bit(1);
    io.overscan      = data.bit(2);
    io.pseudoHires   = data.bit(3);
    io.extbg         = data.bit(6);
    updateVideoMode();
    return;
  }

  }
}

auto PPU::updateVideoMode() -> void {
  self.vdisp = !io.overscan ? 225 : 240;

  switch(io.bgMode) {
  case 0:
    bg1.io.mode = Background::Mode::BPP2;
    bg2.io.mode = Background::Mode::BPP2;
    bg3.io.mode = Background::Mode::BPP2;
    bg4.io.mode = Background::Mode::BPP2;
    memory::assign(bg1.io.priority, 8, 11);
    memory::assign(bg2.io.priority, 7, 10);
    memory::assign(bg3.io.priority, 2,  5);
    memory::assign(bg4.io.priority, 1,  4);
    memory::assign(obj.io.priority, 3,  6, 9, 12);
    break;

  case 1:
    bg1.io.mode = Background::Mode::BPP4;
    bg2.io.mode = Background::Mode::BPP4;
    bg3.io.mode = Background::Mode::BPP2;
    bg4.io.mode = Background::Mode::Inactive;
    if(io.bgPriority) {
      memory::assign(bg1.io.priority, 5,  8);
      memory::assign(bg2.io.priority, 4,  7);
      memory::assign(bg3.io.priority, 1, 10);
      memory::assign(obj.io.priority, 2,  3, 6,  9);
    } else {
      memory::assign(bg1.io.priority, 6,  9);
      memory::assign(bg2.io.priority, 5,  8);
      memory::assign(bg3.io.priority, 1,  3);
      memory::assign(obj.io.priority, 2,  4, 7, 10);
    }
    break;

  case 2:
    bg1.io.mode = Background::Mode::BPP4;
    bg2.io.mode = Background::Mode::BPP4;
    bg3.io.mode = Background::Mode::Inactive;
    bg4.io.mode = Background::Mode::Inactive;
    memory::assign(bg1.io.priority, 3, 7);
    memory::assign(bg2.io.priority, 1, 5);
    memory::assign(obj.io.priority, 2, 4, 6, 8);
    break;

  case 3:
    bg1.io.mode = Background::Mode::BPP8;
    bg2.io.mode = Background::Mode::BPP4;
    bg3.io.mode = Background::Mode::Inactive;
    bg4.io.mode = Background::Mode::Inactive;
    memory::assign(bg1.io.priority, 3, 7);
    memory::assign(bg2.io.priority, 1, 5);
    memory::assign(obj.io.priority, 2, 4, 6, 8);
    break;

  case 4:
    bg1.io.mode = Background::Mode::BPP8;
    bg2.io.mode = Background::Mode::BPP2;
    bg3.io.mode = Background::Mode::Inactive;
    bg4.io.mode = Background::Mode::Inactive;
    memory::assign(bg1.io.priority, 3, 7);
    memory::assign(bg2.io.priority, 1, 5);
    memory::assign(obj.io.priority, 2, 4, 6, 8);
    break;

  case 5:
    bg1.io.mode = Background::Mode::BPP4;
    bg2.io.mode = Background::Mode::BPP2;
    bg3.io.mode = Background::Mode::Inactive;
    bg4.io.mode = Background::Mode::Inactive;
    memory::assign(bg1.io.priority, 3, 7);
    memory::assign(bg2.io.priority, 1, 5);
    memory::assign(obj.io.priority, 2, 4, 6, 8);
    break;

  case 6:
    bg1.io.mode = Background::Mode::BPP4;
    bg2.io.mode = Background::Mode::Inactive;
    bg3.io.mode = Background::Mode::Inactive;
    bg4.io.mode = Background::Mode::Inactive;
    memory::assign(bg1.io.priority, 2, 5);
    memory::assign(obj.io.priority, 1, 3, 4, 6);
    break;

  case 7:
    if(!io.extbg) {
      bg1.io.mode = Background::Mode::Mode7;
      bg2.io.mode = Background::Mode::Inactive;
      bg3.io.mode = Background::Mode::Inactive;
      bg4.io.mode = Background::Mode::Inactive;
      memory::assign(bg1.io.priority, 2);
      memory::assign(obj.io.priority, 1, 3, 4, 5);
    } else {
      bg1.io.mode = Background::Mode::Mode7;
      bg2.io.mode = Background::Mode::Mode7;
      bg3.io.mode = Background::Mode::Inactive;
      bg4.io.mode = Background::Mode::Inactive;
      memory::assign(bg1.io.priority, 3);
      memory::assign(bg2.io.priority, 1, 5);
      memory::assign(obj.io.priority, 2, 4, 6, 7);
    }
    break;
  }
}
