diff --git a/higan/emulator/emulator.hpp b/higan/emulator/emulator.hpp
index f0388436..fa440e01 100644
--- a/higan/emulator/emulator.hpp
+++ b/higan/emulator/emulator.hpp
@@ -12,7 +12,7 @@ using namespace nall;
 
 namespace Emulator {
   static const string Name    = "higan";
-  static const string Version = "106.33";
+  static const string Version = "106.34";
   static const string Author  = "byuu";
   static const string License = "GPLv3";
   static const string Website = "https://byuu.org/";
diff --git a/higan/sfc/ppu-fast/background.cpp b/higan/sfc/ppu-fast/background.cpp
index 46bd08c0..3ecab3fd 100644
--- a/higan/sfc/ppu-fast/background.cpp
+++ b/higan/sfc/ppu-fast/background.cpp
@@ -27,7 +27,7 @@ auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void
   uint hmask = (width << self.tileSize << self.screenSize.bit(0)) - 1;
   uint vmask = (width << self.tileSize << self.screenSize.bit(1)) - 1;
 
-  uint y = this->y - this->y % (1 + io.mosaicSize);
+  uint y = this->y - (self.mosaicEnable ? this->y % (1 + io.mosaicSize) : 0);
   if(hires) {
     hscroll <<= 1;
     if(io.interlace) y = y << 1 | ppu.field();
@@ -72,20 +72,20 @@ auto PPU::Line::renderBackground(PPU::IO::Background& self, uint source) -> void
     uint tileNumber = getTile(self, hoffset, voffset);
     uint mirrorY = tileNumber & 0x8000 ? 7 : 0;
     uint mirrorX = tileNumber & 0x4000 ? 7 : 0;
-    uint tilePriority = tileNumber & 0x2000 ? self.priority[1] : self.priority[0];
+    uint tilePriority = self.priority[bool(tileNumber & 0x2000)];
     uint paletteNumber = tileNumber >> 10 & 7;
     uint paletteIndex = paletteBase + (paletteNumber << paletteShift) & 0xff;
 
-    if(tileWidth  == 4 && (hoffset & 8) - 1 != mirrorX) tileNumber +=  1;
-    if(tileHeight == 4 && (voffset & 8) - 1 != mirrorY) tileNumber += 16;
+    if(tileWidth  == 4 && (bool(hoffset & 8) ^ bool(mirrorX))) tileNumber +=  1;
+    if(tileHeight == 4 && (bool(voffset & 8) ^ bool(mirrorY))) tileNumber += 16;
     tileNumber = (tileNumber & 0x03ff) + tiledataIndex & tileMask;
 
     auto tiledata = ppu.tilecache[self.tileMode] + (tileNumber << 6);
-    tiledata += ((voffset & 7) ^ mirrorY) << 3;
+    tiledata += (voffset & 7 ^ mirrorY) << 3;
 
     for(uint tileX = 0; tileX < 8; tileX++, x++) {
       if(x & width) continue;  //x < 0 || x >= width
-      if(--mosaicCounter == 0) {
+      if(!self.mosaicEnable || --mosaicCounter == 0) {
         mosaicCounter = 1 + io.mosaicSize;
         mosaicPalette = tiledata[tileX ^ mirrorX];
         mosaicPriority = tilePriority;
diff --git a/higan/sfc/ppu-fast/io.cpp b/higan/sfc/ppu-fast/io.cpp
index 52b215b6..d19ec204 100644
--- a/higan/sfc/ppu-fast/io.cpp
+++ b/higan/sfc/ppu-fast/io.cpp
@@ -23,9 +23,13 @@ auto PPU::readVRAM() -> uint16 {
 
 auto PPU::writeVRAM(uint1 byte, uint8 data) -> void {
   if(!io.displayDisable && cpu.vcounter() < vdisp()) return;
+  Line::flush();
   auto address = vramAddress();
   vram[address].byte(byte) = data;
+  updateTiledata(address);
+}
 
+auto PPU::updateTiledata(uint15 address) -> void {
   auto word = vram[address];
   auto line2bpp = tilecache[TileMode::BPP2] + (address.bits(3,14) << 6) + (address.bits(0,2) << 3);
   auto line4bpp = tilecache[TileMode::BPP4] + (address.bits(4,14) << 6) + (address.bits(0,2) << 3);
@@ -48,6 +52,7 @@ auto PPU::readOAM(uint10 address) -> uint8 {
 }
 
 auto PPU::writeOAM(uint10 address, uint8 data) -> void {
+  Line::flush();
   if(!io.displayDisable && cpu.vcounter() < vdisp()) address = latch.oamAddress;
   return writeObject(address, data);
 }
@@ -78,22 +83,22 @@ auto PPU::readIO(uint24 address, uint8 data) -> uint8 {
   case 0x2116: case 0x2118: case 0x2119: case 0x211a:
   case 0x2124: case 0x2125: case 0x2126: case 0x2128:
   case 0x2129: case 0x212a: {
-    return ppu1.mdr;
+    return latch.ppu1.mdr;
   }
 
   case 0x2134: {  //MPYL
     uint24 result = (int16)io.mode7.a * (int8)(io.mode7.b >> 8);
-    return ppu1.mdr = result.byte(0);
+    return latch.ppu1.mdr = result.byte(0);
   }
 
   case 0x2135: {  //MPYM
     uint24 result = (int16)io.mode7.a * (int8)(io.mode7.b >> 8);
-    return ppu1.mdr = result.byte(1);
+    return latch.ppu1.mdr = result.byte(1);
   }
 
   case 0x2136: {  //MPYH
     uint24 result = (int16)io.mode7.a * (int8)(io.mode7.b >> 8);
-    return ppu1.mdr = result.byte(2);
+    return latch.ppu1.mdr = result.byte(2);
   }
 
   case 0x2137: {  //SLHV
@@ -102,77 +107,77 @@ auto PPU::readIO(uint24 address, uint8 data) -> uint8 {
   }
 
   case 0x2138: {  //OAMDATAREAD
-    ppu1.mdr = readOAM(io.oamAddress++);
+    data = readOAM(io.oamAddress++);
     oamSetFirstObject();
-    return ppu1.mdr;
+    return latch.ppu1.mdr = data;
   }
 
   case 0x2139: {  //VMDATALREAD
-    ppu1.mdr = latch.vram.byte(0);
+    data = latch.vram.byte(0);
     if(io.vramIncrementMode == 0) {
       latch.vram = readVRAM();
       io.vramAddress += io.vramIncrementSize;
     }
-    return ppu1.mdr;
+    return latch.ppu1.mdr = data;
   }
 
   case 0x213a: {  //VMDATAHREAD
-    ppu1.mdr = latch.vram.byte(1);
+    data = latch.vram.byte(1);
     if(io.vramIncrementMode == 1) {
       latch.vram = readVRAM();
       io.vramAddress += io.vramIncrementSize;
     }
-    return ppu1.mdr;
+    return latch.ppu1.mdr = data;
   }
 
   case 0x213b: {  //CGDATAREAD
     if(io.cgramAddressLatch++ == 0) {
-      ppu2.mdr.bits(0,7) = readCGRAM(0, io.cgramAddress);
+      latch.ppu2.mdr.bits(0,7) = readCGRAM(0, io.cgramAddress);
     } else {
-      ppu2.mdr.bits(0,6) = readCGRAM(1, io.cgramAddress++);
+      latch.ppu2.mdr.bits(0,6) = readCGRAM(1, io.cgramAddress++);
     }
-    return ppu2.mdr;
+    return latch.ppu2.mdr;
   }
 
   case 0x213c: {  //OPHCT
     if(latch.hcounter++ == 0) {
-      ppu2.mdr.bits(0,7) = io.hcounter.bits(0,7);
+      latch.ppu2.mdr.bits(0,7) = io.hcounter.bits(0,7);
     } else {
-      ppu2.mdr.bit(0) = io.hcounter.bit(8);
+      latch.ppu2.mdr.bit(0) = io.hcounter.bit(8);
     }
-    return ppu2.mdr;
+    return latch.ppu2.mdr;
   }
 
   case 0x213d: {  //OPVCT
     if(latch.vcounter++ == 0) {
-      ppu2.mdr.bits(0,7) = io.vcounter.bits(0,7);
+      latch.ppu2.mdr.bits(0,7) = io.vcounter.bits(0,7);
     } else {
-      ppu2.mdr.bit(0) = io.vcounter.bit(8);
+      latch.ppu2.mdr.bit(0) = io.vcounter.bit(8);
     }
-    return ppu2.mdr;
+    return latch.ppu2.mdr;
   }
 
   case 0x213e: {  //STAT77
-    ppu1.mdr.bits(0,3) = ppu1.version;
-    ppu1.mdr.bit(5) = 0;
-    ppu1.mdr.bit(6) = io.obj.rangeOver;
-    ppu1.mdr.bit(7) = io.obj.timeOver;
-    return ppu1.mdr;
+    latch.ppu1.mdr.bits(0,3) = 1;  //PPU1 version
+    latch.ppu1.mdr.bit(5) = 0;
+    latch.ppu1.mdr.bit(6) = io.obj.rangeOver;
+    latch.ppu1.mdr.bit(7) = io.obj.timeOver;
+    return latch.ppu1.mdr;
   }
 
   case 0x213f: {  //STAT78
     latch.hcounter = 0;
     latch.vcounter = 0;
-    ppu2.mdr.bits(0,3) = ppu2.version;
-    ppu2.mdr.bit(4) = Region::PAL();  //0 = NTSC, 1 = PAL
+    latch.ppu2.mdr.bits(0,3) = 3;  //PPU2 version
+    latch.ppu2.mdr.bit(4) = Region::PAL();  //0 = NTSC, 1 = PAL
     if(!cpu.pio().bit(7)) {
-      ppu2.mdr.bit(6) = 1;
+      latch.ppu2.mdr.bit(6) = 1;
     } else {
-      ppu2.mdr.bit(6) = latch.counters;
+      latch.ppu2.mdr.bit(6) = latch.counters;
       latch.counters = 0;
     }
-    ppu2.mdr.bit(7) = field();
-    return ppu2.mdr;
+    latch.ppu2.mdr.bit(7) = field();
+    return latch.ppu2.mdr;
   }
 
   }
@@ -286,9 +291,9 @@ auto PPU::writeIO(uint24 address, uint8 data) -> void {
     io.mode7.hoffset = data << 8 | latch.mode7;
     latch.mode7 = data;
 
-    io.bg1.hoffset = data << 8 | (latch.bgofsPPU1 & ~7) | (latch.bgofsPPU2 & 7);
-    latch.bgofsPPU1 = data;
-    latch.bgofsPPU2 = data;
+    io.bg1.hoffset = data << 8 | (latch.ppu1.bgofs & ~7) | (latch.ppu2.bgofs & 7);
+    latch.ppu1.bgofs = data;
+    latch.ppu2.bgofs = data;
     return;
   }
 
@@ -296,47 +301,47 @@ auto PPU::writeIO(uint24 address, uint8 data) -> void {
     io.mode7.voffset = data << 8 | latch.mode7;
     latch.mode7 = data;
 
-    io.bg1.voffset = data << 8 | latch.bgofsPPU1;
-    latch.bgofsPPU1 = data;
+    io.bg1.voffset = data << 8 | latch.ppu1.bgofs;
+    latch.ppu1.bgofs = data;
     return;
   }
 
   case 0x210f: {  //BG2HOFS
-    io.bg2.hoffset = data << 8 | (latch.bgofsPPU1 & ~7) | (latch.bgofsPPU2 & 7);
-    latch.bgofsPPU1 = data;
-    latch.bgofsPPU2 = data;
+    io.bg2.hoffset = data << 8 | (latch.ppu1.bgofs & ~7) | (latch.ppu2.bgofs & 7);
+    latch.ppu1.bgofs = data;
+    latch.ppu2.bgofs = data;
     return;
   }
 
   case 0x2110: {  //BG2VOFS
-    io.bg2.voffset = data << 8 | latch.bgofsPPU1;
-    latch.bgofsPPU1 = data;
+    io.bg2.voffset = data << 8 | latch.ppu1.bgofs;
+    latch.ppu1.bgofs = data;
     return;
   }
 
   case 0x2111: {  //BG3HOFS
-    io.bg3.hoffset = data << 8 | (latch.bgofsPPU1 & ~7) | (latch.bgofsPPU2 & 7);
-    latch.bgofsPPU1 = data;
-    latch.bgofsPPU2 = data;
+    io.bg3.hoffset = data << 8 | (latch.ppu1.bgofs & ~7) | (latch.ppu2.bgofs & 7);
+    latch.ppu1.bgofs = data;
+    latch.ppu2.bgofs = data;
     return;
   }
 
   case 0x2112: {  //BG3VOFS
-    io.bg3.voffset = data << 8 | latch.bgofsPPU1;
-    latch.bgofsPPU1 = data;
+    io.bg3.voffset = data << 8 | latch.ppu1.bgofs;
+    latch.ppu1.bgofs = data;
     return;
   }
 
   case 0x2113: {  //BG4HOFS
-    io.bg4.hoffset = data << 8 | (latch.bgofsPPU1 & ~7) | (latch.bgofsPPU2 & 7);
-    latch.bgofsPPU1 = data;
-    latch.bgofsPPU2 = data;
+    io.bg4.hoffset = data << 8 | (latch.ppu1.bgofs & ~7) | (latch.ppu2.bgofs & 7);
+    latch.ppu1.bgofs = data;
+    latch.ppu2.bgofs = data;
     return;
   }
 
   case 0x2114: {  //BG4VOFS
-    io.bg4.voffset = data << 8 | latch.bgofsPPU1;
-    latch.bgofsPPU1 = data;
+    io.bg4.voffset = data << 8 | latch.ppu1.bgofs;
+    latch.ppu1.bgofs = data;
     return;
   }
 
diff --git a/higan/sfc/ppu-fast/line.cpp b/higan/sfc/ppu-fast/line.cpp
index 33a6af81..d92d3489 100644
--- a/higan/sfc/ppu-fast/line.cpp
+++ b/higan/sfc/ppu-fast/line.cpp
@@ -1,3 +1,17 @@
+uint PPU::Line::start = 0;
+uint PPU::Line::count = 0;
+
+auto PPU::Line::flush() -> void {
+  if(Line::count) {
+    #pragma omp parallel for
+    for(uint y = 0; y < Line::count; y++) {
+      ppu.lines[Line::start + y].render();
+    }
+    Line::start = 0;
+    Line::count = 0;
+  }
+}
+
 auto PPU::Line::render() -> void {
   bool hires = io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
 
@@ -16,7 +30,8 @@ auto PPU::Line::render() -> void {
   renderBackground(io.bg4, Source::BG4);
   renderObject(io.obj);
 
-  auto output = !ppu.interlace() || !ppu.field() ? outputLo : outputHi;
+  auto output = ppu.output + y * 1024;
+  if(ppu.interlace() && ppu.field()) output += 512;
   auto width = !ppu.hires() ? 256 : 512;
   auto luma = io.displayBrightness << 15;
 
@@ -29,13 +44,14 @@ auto PPU::Line::render() -> void {
   renderWindow(io.col.window, io.col.window.belowMask, windowBelow);
 
   if(width == 256) for(uint x : range(width)) {
-    output[x] = luma | pixel(x, above[x], below[x]);
+    *output++ = luma | pixel(x, above[x], below[x]);
   } else if(!hires) for(uint x : range(256)) {
-    output[x << 1 | 0] =
-    output[x << 1 | 1] = luma | pixel(x, above[x], below[x]);
+    auto color = luma | pixel(x, above[x], below[x]);
+    *output++ = color;
+    *output++ = color;
   } else for(uint x : range(256)) {
-    output[x << 1 | 0] = luma | pixel(x, below[x], above[x]);
-    output[x << 1 | 1] = luma | pixel(x, above[x], below[x]);
+    *output++ = luma | pixel(x, below[x], above[x]);
+    *output++ = luma | pixel(x, above[x], below[x]);
   }
 }
 
@@ -74,9 +90,9 @@ auto PPU::Line::directColor(uint palette, uint tile) const -> uint15 {
 }
 
 auto PPU::Line::plotAbove(uint x, uint source, uint priority, uint color) -> void {
-  if(priority >= above[x].priority) above[x] = {source, priority, color};
+  if(priority > above[x].priority) above[x] = {source, priority, color};
 }
 
 auto PPU::Line::plotBelow(uint x, uint source, uint priority, uint color) -> void {
-  if(priority >= below[x].priority) below[x] = {source, priority, color};
+  if(priority > below[x].priority) below[x] = {source, priority, color};
 }
diff --git a/higan/sfc/ppu-fast/mode7.cpp b/higan/sfc/ppu-fast/mode7.cpp
index 06c7c6bd..6543818f 100644
--- a/higan/sfc/ppu-fast/mode7.cpp
+++ b/higan/sfc/ppu-fast/mode7.cpp
@@ -1,5 +1,5 @@
 auto PPU::Line::renderMode7(PPU::IO::Background& self, uint source) -> void {
-  int Y = this->y - this->y % (1 + io.mosaicSize);
+  int Y = this->y - (self.mosaicEnable ? this->y % (1 + io.mosaicSize) : 0);
   int y = !io.mode7.vflip ? Y : 255 - Y;
 
   int a = (int16)io.mode7.a;
@@ -47,7 +47,7 @@ auto PPU::Line::renderMode7(PPU::IO::Background& self, uint source) -> void {
       palette &= 0x7f;
     }
 
-    if(--mosaicCounter == 0) {
+    if(!self.mosaicEnable || --mosaicCounter == 0) {
       mosaicCounter = 1 + io.mosaicSize;
       mosaicPalette = palette;
       mosaicPriority = priority;
diff --git a/higan/sfc/ppu-fast/object.cpp b/higan/sfc/ppu-fast/object.cpp
index a27547e7..87ee5d04 100644
--- a/higan/sfc/ppu-fast/object.cpp
+++ b/higan/sfc/ppu-fast/object.cpp
@@ -9,8 +9,8 @@ auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
 
   uint itemCount = 0;
   uint tileCount = 0;
-  for(auto n : range(32)) items[n].valid = false;
-  for(auto n : range(34)) tiles[n].valid = false;
+  for(auto& item : items) item.valid = false;
+  for(auto& tile : tiles) tile.valid = false;
 
   for(auto n : range(128)) {
     ObjectItem item{true, self.first + n};
@@ -94,6 +94,9 @@ auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
   ppu.io.obj.rangeOver |= itemCount > 32;
   ppu.io.obj.timeOver  |= tileCount > 34;
 
+  uint8 palette[256];
+  uint8 priority[256];
+
   for(uint n : range(34)) {
     const auto& tile = tiles[n];
     if(!tile.valid) continue;
@@ -105,16 +108,20 @@ auto PPU::Line::renderObject(PPU::IO::Object& self) -> void {
       tileX &= 511;
       if(tileX < 256) {
         if(uint color = tiledata[x ^ mirrorX]) {
-          uint source = tile.palette < 192 ? Source::OBJ1 : Source::OBJ2;
-          uint priority = self.priority[tile.priority];
-          color = cgram[tile.palette + color];
-          if(self.aboveEnable && !windowAbove[x]) plotAbove(tileX, source, priority, color);
-          if(self.belowEnable && !windowBelow[x]) plotBelow(tileX, source, priority, color);
+          palette[tileX] = tile.palette + color;
+          priority[tileX] = self.priority[tile.priority];
         }
       }
       tileX++;
     }
   }
+
+  for(uint x : range(256)) {
+    if(!priority[x]) continue;
+    uint source = palette[x] < 192 ? Source::OBJ1 : Source::OBJ2;
+    if(self.aboveEnable && !windowAbove[x]) plotAbove(x, source, priority[x], cgram[palette[x]]);
+    if(self.belowEnable && !windowBelow[x]) plotBelow(x, source, priority[x], cgram[palette[x]]);
+  }
 }
 
 auto PPU::oamAddressReset() -> void {
diff --git a/higan/sfc/ppu-fast/ppu.cpp b/higan/sfc/ppu-fast/ppu.cpp
index 84b4e29c..209c9ef9 100644
--- a/higan/sfc/ppu-fast/ppu.cpp
+++ b/higan/sfc/ppu-fast/ppu.cpp
@@ -13,9 +13,6 @@ PPU ppu;
 #include <sfc/ppu/counter/serialization.cpp>
 
 PPU::PPU() {
-  ppu1.version = 1;
-  ppu2.version = 3;
-
   output = new uint32[512 * 512];
   output += 16 * 512;  //overscan offset
 
@@ -25,18 +22,12 @@ PPU::PPU() {
 
   for(uint y : range(240)) {
     lines[y].y = y;
-    lines[y].outputLo = output + (y * 2 + 0) * 512;
-    lines[y].outputHi = output + (y * 2 + 1) * 512;
   }
 }
 
 PPU::~PPU() {
   output -= 16 * 512;  //overscan offset
   delete[] output;
-
-  delete[] tilecache[TileMode::BPP2];
-  delete[] tilecache[TileMode::BPP4];
-  delete[] tilecache[TileMode::BPP8];
 }
 
 auto PPU::Enter() -> void {
@@ -54,24 +45,25 @@ auto PPU::main() -> void {
   uint y = vcounter();
   step(512);
   if(y >= 1 && y <= vdisp()) {
-    memory::copy(&lines[y].cgram, &cgram, sizeof(cgram));
-    memory::copy(&lines[y].io, &io, sizeof(io));
-    //lines[y].render();
+    memcpy(&lines[y].io, &io, sizeof(io));
+    memcpy(&lines[y].cgram, &cgram, sizeof(cgram));
+    if(!Line::count) Line::start = y;
+    Line::count++;
   }
   step(lineclocks() - hcounter());
 }
 
 auto PPU::scanline() -> void {
   if(vcounter() == 0) {
-    frame.interlace = io.interlace;
-    frame.overscan = io.overscan;
-    frame.hires = false;
+    latch.interlace = io.interlace;
+    latch.overscan = io.overscan;
+    latch.hires = false;
     io.obj.timeOver = false;
     io.obj.rangeOver = false;
   }
 
   if(vcounter() > 0 && vcounter() < vdisp()) {
-    frame.hires |= io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
+    latch.hires |= io.pseudoHires || io.bgMode == 5 || io.bgMode == 6;
   }
 
   if(vcounter() == vdisp() && !io.displayDisable) {
@@ -79,11 +71,7 @@ auto PPU::scanline() -> void {
   }
 
   if(vcounter() == 240) {
-    const uint limit = vdisp();
-    #pragma omp parallel for
-    for(uint y = 1; y < limit; y++) {
-      lines[y].render();
-    }
+    Line::flush();
     scheduler.exit(Scheduler::Event::Frame);
   }
 }
@@ -94,7 +82,9 @@ auto PPU::refresh() -> void {
   auto pitch  = 512 << !interlace();
   auto width  = 256 << hires();
   auto height = 240 << interlace();
+  if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, false);
   Emulator::video.refresh(output, pitch * sizeof(uint32), width, height);
+  if(!hires()) Emulator::video.setEffect(Emulator::Video::Effect::ColorBleed, settings.blurEmulation);
 }
 
 auto PPU::load(Markup::Node node) -> bool {
@@ -109,6 +99,21 @@ auto PPU::power(bool reset) -> void {
   function<auto (uint24, uint8) -> uint8> reader{&PPU::readIO, this};
   function<auto (uint24, uint8) -> void> writer{&PPU::writeIO, this};
   bus.map(reader, writer, "00-3f,80-bf:2100-213f");
+
+  if(!reset) {
+    for(auto address : range(32768)) {
+      vram[address] = 0x0000;
+      updateTiledata(address);
+    }
+    for(auto& color : cgram) color = 0x0000;
+    for(auto& object : objects) object = {};
+  }
+
+  latch = {};
+  io = {};
+
+  Line::start = 0;
+  Line::count = 0;
 }
 
 }
diff --git a/higan/sfc/ppu-fast/ppu.hpp b/higan/sfc/ppu-fast/ppu.hpp
index bef8fa2c..ea21277d 100644
--- a/higan/sfc/ppu-fast/ppu.hpp
+++ b/higan/sfc/ppu-fast/ppu.hpp
@@ -2,13 +2,13 @@
 
 //limitations:
 //* mid-scanline effects not support
-//* mid-frame OAM changes not supported
-//* range-time over flags not reported in real-time
+//* vertical mosaic coordinates are not exact
+//* (hardware-mod) 128KB VRAM mode not supported
 
 struct PPU : Thread, PPUcounter {
-  alwaysinline auto interlace() const -> bool { return frame.interlace; }
-  alwaysinline auto overscan() const -> bool { return frame.overscan; }
-  alwaysinline auto hires() const -> bool { return frame.hires; }
+  alwaysinline auto interlace() const -> bool { return latch.interlace; }
+  alwaysinline auto overscan() const -> bool { return latch.overscan; }
+  alwaysinline auto hires() const -> bool { return latch.hires; }
   alwaysinline auto vdisp() const -> uint { return !io.overscan ? 225 : 240; }
 
   //ppu.cpp
@@ -27,49 +27,43 @@ struct PPU : Thread, PPUcounter {
   auto serialize(serializer&) -> void;
 
 public:
-  uint32* output = nullptr;
-  uint8* tilecache[3] = {};  //bitplane -> bitmap tiledata
-  uint16 vram[32 * 1024];
-  uint16 cgram[256];
-
-  struct {
-    uint4 version;
-    uint8 mdr;
-  } ppu1, ppu2;
+  struct Source { enum : uint { BG1, BG2, BG3, BG4, OBJ1, OBJ2, COL }; };
+  struct TileMode { enum : uint { BPP2, BPP4, BPP8, Mode7, Inactive }; };
+  struct ScreenMode { enum : uint { Above, Below }; };
 
   struct Latch {
+    //serialization.cpp
+    auto serialize(serializer&) -> void;
+
+    uint1 interlace;
+    uint1 overscan;
+    uint1 hires;
+
     uint16 vram;
     uint8  oam;
     uint8  cgram;
-    uint8  bgofsPPU1;
-    uint8  bgofsPPU2;
+
+    uint10 oamAddress;
+    uint8  cgramAddress;
+
     uint8  mode7;
     uint1  counters;
     uint1  hcounter;  //hdot
     uint1  vcounter;
 
-    uint10 oamAddress;
-    uint8  cgramAddress;
-  } latch;
+    struct PPU {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
 
-  //io.cpp
-  auto latchCounters() -> void;
-  alwaysinline auto vramAddress() const -> uint15;
-  alwaysinline auto readVRAM() -> uint16;
-  alwaysinline auto writeVRAM(uint1 byte, uint8 data) -> void;
-  alwaysinline auto readOAM(uint10 address) -> uint8;
-  alwaysinline auto writeOAM(uint10 address, uint8 data) -> void;
-  alwaysinline auto readCGRAM(uint1 byte, uint8 address) -> uint8;
-  alwaysinline auto writeCGRAM(uint8 address, uint15 data) -> void;
-  auto readIO(uint24 address, uint8 data) -> uint8;
-  auto writeIO(uint24 address, uint8 data) -> void;
-  auto updateVideoMode() -> void;
-
-  struct Source { enum : uint { BG1, BG2, BG3, BG4, OBJ1, OBJ2, COL }; };
-  struct TileMode { enum : uint { BPP2, BPP4, BPP8, Mode7, Inactive }; };
-  struct ScreenMode { enum : uint { Above, Below }; };
+      uint8 mdr;
+      uint8 bgofs;
+    } ppu1, ppu2;
+  };
 
   struct IO {
+    //serialization.cpp
+    auto serialize(serializer&) -> void;
+
     uint1  displayDisable;
     uint4  displayBrightness;
     uint10 oamBaseAddress;
@@ -91,34 +85,10 @@ public:
     uint1  pseudoHires;
     uint1  extbg;
 
-    struct WindowLayer {
-      uint1 oneEnable;
-      uint1 oneInvert;
-      uint1 twoEnable;
-      uint1 twoInvert;
-      uint2 mask;
-      uint1 aboveEnable;
-      uint1 belowEnable;
-    };
-
-    struct WindowColor {
-      uint1 oneEnable;
-      uint1 oneInvert;
-      uint1 twoEnable;
-      uint1 twoInvert;
-      uint2 mask;
-      uint2 aboveMask;
-      uint2 belowMask;
-    };
-
-    struct Window {
-      uint8 oneLeft;
-      uint8 oneRight;
-      uint8 twoLeft;
-      uint8 twoRight;
-    } window;
-
     struct Mode7 {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
       uint1  hflip;
       uint1  vflip;
       uint2  repeat;
@@ -132,7 +102,46 @@ public:
       uint16 voffset;
     } mode7;
 
+    struct Window {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
+      uint8 oneLeft;
+      uint8 oneRight;
+      uint8 twoLeft;
+      uint8 twoRight;
+    } window;
+
+    struct WindowLayer {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
+      uint1 oneEnable;
+      uint1 oneInvert;
+      uint1 twoEnable;
+      uint1 twoInvert;
+      uint2 mask;
+      uint1 aboveEnable;
+      uint1 belowEnable;
+    };
+
+    struct WindowColor {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
+      uint1 oneEnable;
+      uint1 oneInvert;
+      uint1 twoEnable;
+      uint1 twoInvert;
+      uint2 mask;
+      uint2 aboveMask;
+      uint2 belowMask;
+    };
+
     struct Background {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
       WindowLayer window;
       uint1  aboveEnable;
       uint1  belowEnable;
@@ -148,6 +157,9 @@ public:
     } bg1, bg2, bg3, bg4;
 
     struct Object {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
       WindowLayer window;
       uint1  aboveEnable;
       uint1  belowEnable;
@@ -162,6 +174,9 @@ public:
     } obj;
 
     struct Color {
+      //serialization.cpp
+      auto serialize(serializer&) -> void;
+
       WindowColor window;
       uint1  enable[7];
       uint1  directColor;
@@ -170,21 +185,12 @@ public:
       uint1  mathMode;   //0 = add; 1 = sub
       uint15 fixedColor;
     } col;
-  } io;
-
-  struct Frame {
-    uint1 interlace;
-    uint1 overscan;
-    uint1 hires;
-  } frame;
-
-  //object.cpp
-  auto oamAddressReset() -> void;
-  auto oamSetFirstObject() -> void;
-  auto readObject(uint10 address) -> uint8;
-  auto writeObject(uint10 address, uint8 data) -> void;
+  };
 
   struct Object {
+    //serialization.cpp
+    auto serialize(serializer&) -> void;
+
     uint9 x;
     uint8 y;
     uint8 character;
@@ -194,12 +200,66 @@ public:
     uint2 priority;
     uint3 palette;
     uint1 size;
-  } objects[128];
+  };
+
+  struct ObjectItem {
+    uint1 valid;
+    uint7 index;
+    uint8 width;
+    uint8 height;
+  };
+
+  struct ObjectTile {
+    uint1  valid;
+    uint9  x;
+    uint8  y;
+    uint2  priority;
+    uint8  palette;
+    uint1  hflip;
+    uint11 number;
+  };
+
+  struct Pixel {
+    uint source;
+    uint priority;
+    uint color;
+  };
+
+  //io.cpp
+  auto latchCounters() -> void;
+  alwaysinline auto vramAddress() const -> uint15;
+  alwaysinline auto readVRAM() -> uint16;
+  alwaysinline auto writeVRAM(uint1 byte, uint8 data) -> void;
+  alwaysinline auto updateTiledata(uint15 address) -> void;
+  alwaysinline auto readOAM(uint10 address) -> uint8;
+  alwaysinline auto writeOAM(uint10 address, uint8 data) -> void;
+  alwaysinline auto readCGRAM(uint1 byte, uint8 address) -> uint8;
+  alwaysinline auto writeCGRAM(uint8 address, uint15 data) -> void;
+  auto readIO(uint24 address, uint8 data) -> uint8;
+  auto writeIO(uint24 address, uint8 data) -> void;
+  auto updateVideoMode() -> void;
+
+  //object.cpp
+  auto oamAddressReset() -> void;
+  auto oamSetFirstObject() -> void;
+  auto readObject(uint10 address) -> uint8;
+  auto writeObject(uint10 address, uint8 data) -> void;
+
+  //[serialized]
+  Latch latch;
+  IO io;
+
+  uint16 vram[32 * 1024];
+  uint15 cgram[256];
+  Object objects[128];
+
+  //[unserialized]
+  uint32* output = nullptr;
+  uint8*  tilecache[3] = {};  //bitplane -> bitmap tiledata
 
   struct Line {
-    struct Pixel;
-
     //line.cpp
+    static auto flush() -> void;
     auto render() -> void;
     auto pixel(uint x, Pixel above, Pixel below) const -> uint15;
     auto blend(uint x, uint y, bool halve) const -> uint15;
@@ -221,38 +281,24 @@ public:
     auto renderWindow(PPU::IO::WindowLayer&, bool, bool*) -> void;
     auto renderWindow(PPU::IO::WindowColor&, uint, bool*) -> void;
 
-    uint9 y;
-    uint32* outputLo = nullptr;
-    uint32* outputHi = nullptr;
+    //[unserialized]
+    uint9 y;  //constant
 
-    uint15 cgram[256];
     IO io;
+    uint15 cgram[256];
 
-    struct ObjectItem {
-      uint1 valid;
-      uint7 index;
-      uint8 width;
-      uint8 height;
-    } items[32];
+    ObjectItem items[32];
+    ObjectTile tiles[34];
 
-    struct ObjectTile {
-      uint1  valid;
-      uint9  x;
-      uint8  y;
-      uint2  priority;
-      uint8  palette;
-      uint1  hflip;
-      uint11 number;
-    } tiles[34];
-
-    struct Pixel {
-      uint source;
-      uint priority;
-      uint color;
-    } above[256], below[256];
+    Pixel above[256];
+    Pixel below[256];
 
     bool windowAbove[256];
     bool windowBelow[256];
+
+    //flush()
+    static uint start;
+    static uint count;
   } lines[240];
 };
 
diff --git a/higan/sfc/ppu-fast/serialization.cpp b/higan/sfc/ppu-fast/serialization.cpp
index 6abea464..ef96687c 100644
--- a/higan/sfc/ppu-fast/serialization.cpp
+++ b/higan/sfc/ppu-fast/serialization.cpp
@@ -1,4 +1,160 @@
 auto PPU::serialize(serializer& s) -> void {
   Thread::serialize(s);
   PPUcounter::serialize(s);
+
+  latch.serialize(s);
+  io.serialize(s);
+  s.array(vram);
+  s.array(cgram);
+  for(auto& object : objects) object.serialize(s);
+
+  for(auto address : range(32768)) updateTiledata(address);
+  Line::start = 0;
+  Line::count = 0;
+}
+
+auto PPU::Latch::serialize(serializer& s) -> void {
+  s.integer(interlace);
+  s.integer(overscan);
+  s.integer(hires);
+  s.integer(vram);
+  s.integer(oam);
+  s.integer(cgram);
+  s.integer(oamAddress);
+  s.integer(cgramAddress);
+  s.integer(mode7);
+  s.integer(counters);
+  s.integer(hcounter);
+  s.integer(vcounter);
+  ppu1.serialize(s);
+  ppu2.serialize(s);
+}
+
+auto PPU::Latch::PPU::serialize(serializer& s) -> void {
+  s.integer(mdr);
+  s.integer(bgofs);
+}
+
+auto PPU::IO::serialize(serializer& s) -> void {
+  s.integer(displayDisable);
+  s.integer(displayBrightness);
+  s.integer(oamBaseAddress);
+  s.integer(oamAddress);
+  s.integer(oamPriority);
+  s.integer(bgPriority);
+  s.integer(bgMode);
+  s.integer(mosaicSize);
+  s.integer(vramIncrementMode);
+  s.integer(vramMapping);
+  s.integer(vramIncrementSize);
+  s.integer(vramAddress);
+  s.integer(cgramAddress);
+  s.integer(cgramAddressLatch);
+  s.integer(hcounter);
+  s.integer(vcounter);
+  s.integer(interlace);
+  s.integer(overscan);
+  s.integer(pseudoHires);
+  s.integer(extbg);
+
+  mode7.serialize(s);
+  window.serialize(s);
+  bg1.serialize(s);
+  bg2.serialize(s);
+  bg3.serialize(s);
+  bg4.serialize(s);
+  obj.serialize(s);
+  col.serialize(s);
+}
+
+auto PPU::IO::Mode7::serialize(serializer& s) -> void {
+  s.integer(hflip);
+  s.integer(vflip);
+  s.integer(repeat);
+  s.integer(a);
+  s.integer(b);
+  s.integer(c);
+  s.integer(d);
+  s.integer(x);
+  s.integer(y);
+  s.integer(hoffset);
+  s.integer(voffset);
+}
+
+auto PPU::IO::Window::serialize(serializer& s) -> void {
+  s.integer(oneLeft);
+  s.integer(oneRight);
+  s.integer(twoLeft);
+  s.integer(twoRight);
+}
+
+auto PPU::IO::WindowLayer::serialize(serializer& s) -> void {
+  s.integer(oneEnable);
+  s.integer(oneInvert);
+  s.integer(twoEnable);
+  s.integer(twoInvert);
+  s.integer(mask);
+  s.integer(aboveEnable);
+  s.integer(belowEnable);
+}
+
+auto PPU::IO::WindowColor::serialize(serializer& s) -> void {
+  s.integer(oneEnable);
+  s.integer(oneInvert);
+  s.integer(twoEnable);
+  s.integer(twoInvert);
+  s.integer(mask);
+  s.integer(aboveMask);
+  s.integer(belowMask);
+}
+
+auto PPU::IO::Background::serialize(serializer& s) -> void {
+  window.serialize(s);
+  s.integer(aboveEnable);
+  s.integer(belowEnable);
+  s.integer(mosaicEnable);
+  s.integer(tiledataAddress);
+  s.integer(screenAddress);
+  s.integer(screenSize);
+  s.integer(tileSize);
+  s.integer(hoffset);
+  s.integer(voffset);
+  s.integer(tileMode);
+  s.array(priority);
+}
+
+auto PPU::IO::Object::serialize(serializer& s) -> void {
+  window.serialize(s);
+  s.integer(aboveEnable);
+  s.integer(belowEnable);
+  s.integer(interlace);
+  s.integer(baseSize);
+  s.integer(nameselect);
+  s.integer(tiledataAddress);
+  s.integer(first);
+  s.integer(rangeOver);
+  s.integer(timeOver);
+  s.array(priority);
+}
+
+auto PPU::IO::Color::serialize(serializer& s) -> void {
+  window.serialize(s);
+  s.array(enable);
+  s.integer(directColor);
+  s.integer(blendMode);
+  s.integer(halve);
+  s.integer(mathMode);
+  s.integer(fixedColor);
+}
+
+auto PPU::Object::serialize(serializer& s) -> void {
+  s.integer(x);
+  s.integer(y);
+  s.integer(character);
+  s.integer(nameselect);
+  s.integer(vflip);
+  s.integer(hflip);
+  s.integer(priority);
+  s.integer(palette);
+  s.integer(size);
 }
diff --git a/higan/systems/Super Famicom.sys/boards.bml b/higan/systems/Super Famicom.sys/boards.bml
index db280d56..feec2a76 100644
--- a/higan/systems/Super Famicom.sys/boards.bml	
+++ b/higan/systems/Super Famicom.sys/boards.bml	
@@ -1,5 +1,5 @@
 database
-  revision: 2018-05-17
+  revision: 2018-06-01
 
 //Boards (Production)
 
@@ -565,7 +565,7 @@ board: SHVC-YJ0N-01
 //Boards (Generic)
 
 database
-  revision: 2018-05-16
+  revision: 2018-06-01
 
 board: ARM-LOROM-RAM
   memory type=ROM content=Program
@@ -885,7 +885,7 @@ board: SPC7110-RAM-EPSONRTC
     memory type=RAM content=Save
       map address=00-3f,80-bf:6000-7fff mask=0xe000
   rtc manufacturer=Epson
-    map address=00-3f,80-bf:4800-4842
+    map address=00-3f,80-bf:4840-4842
     memory type=RTC content=Time manufacturer=Epson
 
 board: ST-LOROM
diff --git a/higan/target-bsnes/program/interface.cpp b/higan/target-bsnes/program/interface.cpp
index 61a16bf3..b1525329 100644
--- a/higan/target-bsnes/program/interface.cpp
+++ b/higan/target-bsnes/program/interface.cpp
@@ -211,8 +211,8 @@ auto Program::videoRefresh(const uint32* data, uint pitch, uint width, uint heig
 
   pitch >>= 2;
   if(presentation->overscanCropping.checked()) {
-    data += 16 * pitch;
-    height -= 32;
+    if(height == 240) data +=  8 * pitch, height -= 16;
+    if(height == 480) data += 16 * pitch, height -= 32;
   }
 
   if(video->lock(output, length, width, height)) {