// The picture processing unit.
//
// The PPU is a dot-driven machine: it emits one pixel per dot, 341 dots per
// scanline, 262 scanlines per frame on NTSC. Everything it renders comes from
// two memories — 8 KiB of character ROM holding the tile bitmaps, and 2 KiB of
// video RAM holding which tile goes where.
//
// The central modelling decision here is that rendering is done per pixel from
// the PPU's current scroll and address state, rather than by copying a completed
// nametable into a frame buffer at the start of each frame. The per-pixel model
// is what makes mid-frame scroll changes — which every side-scroller relies on —
// work without a special case.

///|
/// The width of the visible picture in pixels.
pub const SCREEN_WIDTH : Int32 = 256

///|
/// The height of the visible picture in pixels.
pub const SCREEN_HEIGHT : Int32 = 240

///|
/// Dots per scanline. The visible picture is the first 256; the remaining 85 are
/// the horizontal blanking interval, and they are where the CPU gets its chance
/// to write to PPU registers.
const DOTS_PER_SCANLINE : Int32 = 341

///|
/// The first scanline of the vertical blanking interval on NTSC.
const VBLANK_START : Int32 = 241

///|
/// A palette entry: six bits selecting one of the console's 64 colours, plus the
/// emphasis bits that tint the whole screen.
pub type PaletteIndex = UInt32

///|
/// The PPU's control register at $2000.
///
/// Held as named fields rather than a raw byte so that a test can set the
/// nametable select without also having to know which bits the other fields use.
pub(all) struct Ctrl {
  /// Base nametable selected for rendering; also the high bit of the scroll
  /// address during a scroll write.
  mut nametable : UInt32
  /// Add 32 to the address after each CPU read or write, for column-major fills.
  mut increment32 : Bool
  /// Where sprite bitmaps are fetched from: 0 for $0000, 1 for $1000.
  mut sprite_pattern : UInt32
  /// Where background bitmaps are fetched from.
  mut background_pattern : UInt32
  /// Sprites are 8x16 rather than 8x8.
  mut tall_sprites : Bool
  /// Generate an NMI at the start of each vertical blank.
  mut nmi_enabled : Bool
}

///|
/// The PPU's mask register at $2001.
pub(all) struct Mask {
  /// Show the background in the leftmost 8 pixels.
  mut show_background_left : Bool
  /// Show sprites in the leftmost 8 pixels.
  mut show_sprites_left : Bool
  /// Enable background rendering at all.
  mut show_background : Bool
  /// Enable sprite rendering.
  mut show_sprites : Bool
  /// Tint the picture toward red.
  mut emphasize_red : Bool
  /// Tint it toward green.
  mut emphasize_green : Bool
  /// Tint it toward blue.
  mut emphasize_blue : Bool
  /// Greyscale: collapse the palette to its luminance ramp.
  mut greyscale : Bool
}

///|
/// The PPU's status register at $2002.
pub(all) struct Status {
  /// Set during vertical blank; cleared by reading this register.
  mut vblank : Bool
  /// Set when a sprite has hit the sprite-0 pixel; cleared by reading this
  /// register.
  mut sprite_zero_hit : Bool
  /// Set when more than eight sprites share a scanline.
  ///
  /// The hardware's evaluator also has a bug that corrupts the index when an
  /// eighth sprite is found, which games use as a detection signature. This
  /// build raises the flag at the honest moment and does not reproduce the
  /// bug, so the flag is right but a game relying on the corruption will not
  /// see it.
  mut overflow : Bool
}

///|
/// A complete PPU.
///
/// The two 16-bit address latches are the source of most of the PPU's
/// awkwardness:
///
/// - `v` is the address the PPU renders from; it walks across the nametable as
///   dots are emitted.
/// - `t` is the address a scroll write stages a value into.
/// - `fine_x` is the sub-tile horizontal scroll, which cannot be represented in
///   `t` and so is kept separately.
///
/// The `$2006`/`$2005` write protocol is what moves values into and out of them,
/// and getting it wrong produces a picture that is subtly shifted rather than
/// obviously broken.
pub(all) struct Ppu {
  /// The control register.
  ctrl : Ctrl
  /// The mask register.
  mask : Mask
  /// The status register.
  status : Status
  /// Current rendering address.
  mut v : UInt32
  /// Staging address for scroll and address writes.
  mut t : UInt32
  /// Fine horizontal scroll, 0 to 7.
  mut fine_x : UInt32
  /// True while the next write to `$2005` or `$2006` sets the low byte.
  mut write_latch : Bool
  /// Nametable RAM: two 1 KiB tables, mirrored according to the cartridge.
  nt_ram : FixedArray[Byte]
  /// Palette RAM: 32 bytes, with the sprite entries mirrored down to 16.
  palette : FixedArray[Byte]
  /// Object attribute memory: 64 sprites of 4 bytes each.
  oam : FixedArray[Byte]
  /// The byte most recently read into the PPU's internal read buffer.
  ///
  /// A read from any VRAM address returns the previous buffer's contents and
  /// then refills it, which is why a program that reads a nametable byte twice
  /// gets two different answers.
  mut read_buffer : UInt32
  /// Which scanline the raster is on.
  mut scanline : Int32
  /// Which dot within that scanline.
  mut dot : Int32
  /// The decoded picture, one palette index per pixel.
  frame : FixedArray[Byte]
  /// Set when the frame has been fully drawn and the host should present it.
  mut frame_complete : Bool
  /// Which physical nametables answer the four virtual ones.
  ///
  /// Copying the cartridge's mirroring in at reset rather than reading it from
  /// the cartridge on every fetch matters because a single scanline touches the
  /// nametable hundreds of times; four-way mirroring additionally needs its own
  /// RAM, so the PPU owns it either way.
  mut mirroring : Mirroring
  /// Character memory, owned by the cartridge but read through here.
  mut chr : FixedArray[Byte]
  /// True when the cartridge's character memory is RAM and accepts writes.
  mut chr_ram : Bool
  /// How many scanlines the raster has completed since the counter was last
  /// read.
  ///
  /// The board's scanline counter watches the PPU's A12 line and cannot see the
  /// raster directly, so the console reports edges rather than lines; counting
  /// completed scanlines here is the same signal with less plumbing.
  mut scanlines : UInt32
  /// Which 8 KiB character bank the mapper currently has selected.
  ///
  /// Kept here rather than read from the cartridge on every fetch because a
  /// background row alone is eight pattern reads, and a pattern fetch happens
  /// hundreds of times per scanline; the console refreshes this whenever a
  /// mapper write changes it.
  mut chr_bank : Int64
  /// Per-1 KiB page numbers for the eight pages of the 8 KiB pattern space.
  ///
  /// Coarse boards fill this with the one bank they have selected; MMC3 fills it
  /// page by page from its six character registers. The PPU uses it whenever it
  /// is present, because a pattern fetch is the hottest path in the whole
  /// emulator and resolving a mapper's register layout per fetch would cost more
  /// than the fetch.
  mut chr_pages : FixedArray[Int64]?
  /// The offset `$2003` last wrote, used by direct OAM access.
  mut oam_addr : UInt32
}

///|
/// A PPU with every register at its power-on value.
///
/// `ctrl` starts at zero, which selects nametable 0 and 8x8 sprites; `mask`
/// starts at zero, so nothing renders until a game enables it. That is what makes
/// a game's first frame black rather than random.
pub fn Ppu::new() -> Ppu {
  {
    ctrl: {
      nametable: 0,
      increment32: false,
      sprite_pattern: 0,
      background_pattern: 0,
      tall_sprites: false,
      nmi_enabled: false,
    },
    mask: {
      show_background_left: false,
      show_sprites_left: false,
      show_background: false,
      show_sprites: false,
      emphasize_red: false,
      emphasize_green: false,
      emphasize_blue: false,
      greyscale: false,
    },
    status: { vblank: false, sprite_zero_hit: false, overflow: false, },
    v: 0,
    t: 0,
    fine_x: 0,
    write_latch: false,
    // Four KiB, not two: four-screen mirroring gives each virtual table its own
    // page, and the extra pages live on the cartridge rather than in the PPU.
    // Allocating only two would make table 2 and 3 read past the end of the array.
    nt_ram: FixedArray::make(0x1000, b'\x00'),
    palette: FixedArray::make(32, b'\x00'),
    oam: FixedArray::make(256, b'\x00'),
    read_buffer: 0,
    scanline: 0,
    dot: 0,
    frame: FixedArray::make(SCREEN_WIDTH * SCREEN_HEIGHT, b'\x00'),
    frame_complete: false,
    mirroring: Horizontal,
    chr: FixedArray::make(0x2000, b'\x00'),
    chr_ram: false,
    chr_bank: 0L,
    chr_pages: None,
    scanlines: 0U,
    oam_addr: 0,
  }
}

///|
/// What the background layer contributes at one position.
///
/// `opaque` is separate from the colour because a background pixel can resolve
/// to the backdrop colour and still count as opaque for sprite-zero purposes;
/// conflating the two is what makes a status-bar split fire at the wrong line.
priv struct BackgroundPixel {
  /// The palette entry, or the backdrop colour when nothing was drawn.
  colour : UInt32
  /// True when a tile pixel covered this position, whatever its colour.
  covered : Bool
}

///|
/// Computes the background layer at one position from the current scroll state.
///
/// Kept as one function so that rendering and sprite-zero detection agree by
/// construction rather than by two copies of the same arithmetic.
///
/// The vertical position comes from `v`, not from the scanline: a game that
/// rewrites the scroll register mid-frame is telling the PPU to draw a different
/// part of the nametable from here on, and reading the scanline instead would
/// ignore that.
fn Ppu::background_pixel(self : Ppu, x : Int32, _y : Int32) -> BackgroundPixel {
  let backdrop = self.palette_byte(0) & 0x3F
  let hidden = if x < 8 && !self.mask.show_background_left {
    true
  } else {
    !self.mask.show_background
  }
  if hidden {
    return { colour: backdrop, covered: false, }
  }
  // `v` already encodes the whole position: coarse X, coarse Y, the fine Y scroll
  // and both nametable-select bits. Rebuilding a pixel offset from them and then
  // re-adding the base nametable double-counts the table select, because the base
  // is already folded into bit 10 and bit 11 of `v`.
  let table_x = (self.v >> 10) & 0x03
  let name_x = (self.v & 0x1F).reinterpret_as_int()
  let name_y = ((self.v >> 5) & 0x1F).reinterpret_as_int()
  let tile = self.nametable_byte(table_x, name_x, name_y)
  let fine_y = ((self.v >> 12) & 0x07).reinterpret_as_int()
  let row = self.pattern_row(
    tile,
    fine_y.reinterpret_as_uint(),
    self.ctrl.background_pattern,
  )
  let bit = 7 - self.fine_x.reinterpret_as_int()
  // A background pixel is one bit from the low plane: its colour is always 0 or 1,
  // never 2 or 3. Reading the joined word without masking would pick up the high
  // plane's bit and produce colours the background layer cannot show.
  let low = row & 0xFF
  let pixel = (low >> bit) & 1
  if pixel == 0 {
    return { colour: backdrop, covered: false, }
  }
  let attr = self.attribute_byte(table_x, name_x, name_y)
  let high_bits : Int32 = name_y % 4 / 2 * 2 + name_x % 4 / 2
  let palette_index = (attr >> (high_bits * 2)) & 0x03U
  {
    // The palette's first entry is the backdrop, so a tile pixel of colour 1 maps
    // to entry 1 of its group rather than to entry 0. Adding one for the backdrop
    // as well would shift every background colour by one group.
    colour: self.palette_byte(palette_index * 4 + pixel) & 0x3F,
    covered: true,
  }
}

///|
/// Advances the raster by one dot and renders the pixel it lands on.
///
/// Returns true on the dot that starts vertical blank, so that the caller can
/// raise an NMI without polling. The scanline counters follow the roster the
/// hardware uses: an NTSC frame is 262 lines of 341 dots, and the visible
/// picture ends at line 240.
pub fn Ppu::tick(self : Ppu) -> Bool {
  if self.scanline < VBLANK_START {
    if self.dot < SCREEN_WIDTH {
      self.render_pixel()
    }
  }
  let mut started_vblank = false
  self.dot = self.dot + 1
  // `v` walks across the nametable as dots are emitted: every eighth dot it moves
  // to the next tile column, and at the end of a scanline it moves to the next
  // row. Without this the whole screen is one tile, which is what a static `v`
  // produces and it looks like a rendering bug rather than a missing one.
  if self.rendering_enabled() {
    if self.dot == 256 {
      self.increment_y()
    } else if self.dot == 257 {
      self.copy_horizontal()
    } else if self.dot % 8 == 0 && self.dot < 256 {
      self.increment_x()
    }
  }
  if self.dot >= DOTS_PER_SCANLINE {
    self.dot = 0
    self.scanline = self.scanline + 1
    // A scanline is one A12 rising edge for a board that counts them: the fetch
    // pattern is the same on every line, so the line boundary is a faithful
    // stand-in and costs one increment instead of tracking the bus.
    self.scanlines = self.scanlines + 1U
    if self.scanline == VBLANK_START {
      self.status.vblank = true
      started_vblank = true
      // The vertical blank hands the rest of the frame back to the CPU, and the
      // next frame starts from the top of the picture.
      self.copy_vertical()
    } else if self.scanline >= 262 {
      self.scanline = 0
      self.frame_complete = true
      // The vblank flag is cleared at the start of the next frame, because the
      // hardware clears it when the pre-render line finishes rather than when
      // the CPU reads $2002.
      self.status.vblank = false
      self.copy_vertical()
    }
  }
  started_vblank
}

///|
/// True when either layer is on, which is when the hardware advances the
/// rendering address.
pub fn Ppu::rendering_enabled(self : Ppu) -> Bool {
  self.mask.show_background || self.mask.show_sprites
}

///|
/// Advances the coarse horizontal scroll by one tile, wrapping into the next
/// nametable at the end of a row.
fn Ppu::increment_x(self : Ppu) -> Unit {
  if (self.v & 0x001F) == 0x1F {
    self.v = self.v & 0x7FE0
    // Bit 10 selects which of the two horizontal nametables the row continues
    // into; toggling it is how the PPU walks past the right edge.
    self.v = self.v ^ 0x0400
  } else {
    self.v = self.v + 1
  }
}

///|
/// Advances to the next row of tiles, wrapping into the next vertical nametable
/// after the twenty-ninth row.
///
/// The wrap compares against 29 rather than 31 because the bottom two rows of a
/// nametable are the attribute table, not tiles.
fn Ppu::increment_y(self : Ppu) -> Unit {
  if (self.v & 0x7000) != 0x7000 {
    self.v = self.v + 0x1000
  } else {
    self.v = self.v & 0x0FFF
    let mut coarse_y = (self.v & 0x03E0) >> 5
    if coarse_y == 29 {
      coarse_y = 0
      self.v = self.v ^ 0x0800
    } else if coarse_y == 31 {
      coarse_y = 0
    } else {
      coarse_y = coarse_y + 1
    }
    self.v = (self.v & 0x7C1FU) | (coarse_y << 5)
  }
}

///|
/// Copies the horizontal scroll bits from the staging register into the rendering
/// address, which the hardware does at dot 257 of every scanline.
fn Ppu::copy_horizontal(self : Ppu) -> Unit {
  self.v = (self.v & 0x7BE0U) | (self.t & 0x041FU)
}

///|
/// Copies the vertical scroll bits from the staging register, which the hardware
/// does during the pre-render line and at the start of vertical blank.
fn Ppu::copy_vertical(self : Ppu) -> Unit {
  self.v = (self.v & 0x041FU) | (self.t & 0x7BE0U)
}

///|
/// Writes the palette index for the pixel at the raster's current position.
///
/// Rendering reads the nametable through `v`, whose low bits select the tile
/// column and row; `fine_x` shifts the tile bitmap horizontally within the eight
/// pixels a tile covers. The coarse scroll is already folded into `v` by the
/// scroll registers, which is why a split-screen effect only needs the game to
/// rewrite `$2006` mid-frame.
fn Ppu::render_pixel(self : Ppu) -> Unit {
  let x = self.dot
  let y = self.scanline
  let background = self.background_pixel(x, y)
  let mut index = background.colour
  if self.mask.show_sprites {
    let sprite = self.sprite_pixel(x, y)
    match sprite {
      Some(pixel) => {
        // Sprite zero hit is raised only when an opaque sprite pixel overlaps an
        // opaque background pixel. Raising it whenever sprite zero covers this
        // scanline is the usual shortcut and it breaks every game that splits the
        // screen at a status bar, because the hit arrives a whole scanline early.
        if pixel.is_zero && background.covered {
          self.status.sprite_zero_hit = true
        }
        index = pixel.colour
      }
      None => ()
    }
  }
  if self.mask.greyscale {
    index = index & 0x30
  }
  if y < SCREEN_HEIGHT {
    self.frame[y * SCREEN_WIDTH + x] = (index & 0x3F).to_byte()
  }
}

///|
/// Reads one byte of palette RAM.
///
/// The palette is 32 bytes but only 16 are addressable at a time: the sprite
/// half mirrors $3F10-$3F1F down onto $3F00-$3F0F, and every fourth byte is a
/// mirror of the backdrop colour rather than a real entry. Reproducing both
/// mirrors matters because games write the backdrop through the sprite alias.
fn Ppu::palette_byte(self : Ppu, index : UInt32) -> UInt32 {
  let mut at = index.reinterpret_as_int() & 0x1F
  if at >= 0x10 && at % 4 == 0 {
    at = at - 0x10
  }
  self.palette[at].to_int().reinterpret_as_uint() & 0x3F
}

///|
/// Writes one byte of palette RAM through the same mirrors.
pub fn Ppu::set_palette_byte(
  self : Ppu,
  index : UInt32,
  value : UInt32,
) -> Unit {
  let mut at = index.reinterpret_as_int() & 0x1F
  if at >= 0x10 && at % 4 == 0 {
    at = at - 0x10
  }
  self.palette[at] = (value & 0x3F).to_byte()
}

///|
/// Reads a nametable byte for a virtual table, tile column and tile row.
///
/// `table` selects one of the four virtual tables the PPU can address; the
/// cartridge's mirroring decides which of the two physical 1 KiB pages answers.
fn Ppu::nametable_byte(
  self : Ppu,
  table : UInt32,
  column : Int32,
  row : Int32,
) -> UInt32 {
  let physical = table_ram_base(self.mirroring, table.reinterpret_as_int())
  let at = physical + row * 32 + column
  self.nt_ram[at].to_int().reinterpret_as_uint() & 0xFF
}

///|
/// Reads the attribute-table byte governing a tile.
///
/// The attribute table occupies the last 64 bytes of each nametable and packs
/// four 2-bit palette selections per byte, so a tile's byte is found by dividing
/// its coordinates by four and its field by taking the low or high nibble.
fn Ppu::attribute_byte(
  self : Ppu,
  table : UInt32,
  column : Int32,
  row : Int32,
) -> UInt32 {
  let physical = table_ram_base(self.mirroring, table.reinterpret_as_int())
  let at = physical + 0x3C0 + row / 4 * 8 + column / 4
  self.nt_ram[at].to_int().reinterpret_as_uint() & 0xFF
}

///|
/// Fetches one row of a tile bitmap as two bitplanes joined into one byte.
///
/// A tile's bitmap is stored as two 8-byte planes: the low bits of every pixel
/// in one plane and the high bits in the next. Pixel *n* of the row is
/// `(plane0 >> (7-n) & 1) | ((plane1 >> (7-n) & 1) << 1)`.
fn Ppu::pattern_row(
  self : Ppu,
  tile : UInt32,
  row : UInt32,
  base : UInt32,
) -> UInt32 {
  let offset = base.reinterpret_as_int() +
    tile.reinterpret_as_int() * 16 +
    (row.reinterpret_as_int() & 7)
  let low = self.chr_byte(offset)
  let high = self.chr_byte(offset + 8)
  low | (high << 8)
}

///|
/// The offset into the PPU's 2 KiB of video RAM that a virtual nametable maps to.
///
/// The four virtual tables select among two physical pages, and the cartridge's
/// mirroring decides which page each virtual one sees. Getting this wrong is
/// visible immediately: a horizontally mirrored cartridge run with vertical
/// mirroring scrolls the wrong axis through the level.
fn table_ram_base(mirroring : Mirroring, table : Int32) -> Int32 {
  let selector = table & 0x03
  match mirroring {
    // Vertical mirroring puts the two pages side by side: table 0 and 2 are the
    // left page, 1 and 3 the right.
    Vertical => if selector % 2 == 0 { 0x000 } else { 0x400 }
    // Horizontal mirroring stacks them: 0 and 1 are the top, 2 and 3 the bottom.
    Horizontal => if selector / 2 == 0 { 0x000 } else { 0x400 }
    SingleLow => 0x000
    SingleHigh => 0x400
    // Four-screen mirroring gives each virtual table its own page, which needs
    // 4 KiB; the PPU allocates the extra two pages beyond its nominal 2 KiB.
    FourScreen => selector * 0x400
  }
}

///|
/// Reads a byte of character memory.
fn Ppu::chr_byte(self : Ppu, offset : Int32) -> UInt32 {
  let size = self.chr.length()
  if size <= 0 {
    return 0U
  }
  // A board with 1 KiB granularity hands over a page table; using it here keeps
  // the mapper's register layout out of the fetch path.
  match self.chr_pages {
    Some(pages) => {
      let page = offset % 0x2000 / 0x400
      let bank = pages[page].to_int()
      let within = offset % 0x400
      return self.chr[(bank * 0x400 + within) % size]
        .to_int()
        .reinterpret_as_uint() &
        0xFF
    }
    None => ()
  }
  // Pattern tables are 8 KiB, and a board with more than one of them answers a
  // fetch from whichever bank its register currently selects. Adding the bank
  // base before the mask matters: masking first would pin every fetch to bank
  // zero, which renders banks 1..n as a copy of the first.
  let banks = size / 0x2000
  let bank = if banks <= 0 { 0 } else { self.chr_bank.to_int() % banks }
  let at = (bank * 0x2000 + offset % 0x2000) % size
  self.chr[at].to_int().reinterpret_as_uint() & 0xFF
}

///|
/// Renders the sprite layer for one pixel, returning the palette index when a
/// sprite is opaque there.
///
/// Sprites are drawn in OAM order and the first opaque pixel wins, except that
/// sprite 0 hitting an opaque background pixel raises the sprite-zero flag,
/// which is the only way a game can synchronise to the raster.

///|
/// An opaque sprite pixel together with which sprite it came from.
///
/// The index is carried out because sprite zero's hit flag depends on the sprite's
/// position in OAM, and OAM is the only fixed point a game can rely on to know
/// when the raster has reached a particular line.
priv struct SpritePixel {
  /// The palette entry to draw.
  colour : UInt32
  /// True when this came from the sprite at OAM index zero.
  is_zero : Bool
}

///|
/// Finds the frontmost opaque sprite pixel at a position.
///
/// Sprites are drawn in OAM order and the first opaque pixel wins, so a sprite
/// with a higher index that overlaps a lower one is hidden behind it. That is the
/// opposite of the background layer, where nothing overlaps, and it is what makes
/// hardware-sprite multiplexing possible.
fn Ppu::sprite_pixel(self : Ppu, x : Int32, y : Int32) -> SpritePixel? {
  if x < 8 && !self.mask.show_sprites_left {
    return None
  }
  let height = if self.ctrl.tall_sprites { 16 } else { 8 }
  let mut drawn = 0
  for i in 0..<64 {
    let at = i * 4
    let sprite_y = self.oam[at].to_int().reinterpret_as_uint()
    let tile_index = self.oam[at + 1].to_int().reinterpret_as_uint()
    let attributes = self.oam[at + 2].to_int().reinterpret_as_uint()
    let sprite_x = self.oam[at + 3].to_int().reinterpret_as_uint()
    // OAM stores the sprite's top edge minus one, so $FF means the sprite starts
    // on scanline 0. The addition wraps inside eight bits rather than going
    // negative, which is why $FF is the top of the screen rather than above it.
    let top = ((sprite_y + 1U) & 0xFFU).reinterpret_as_int()
    let row = y - top
    // A negative difference means the scanline is above the sprite; a distance at
    // least the sprite's height means it is below.
    if row < 0 || row >= height {
      continue
    }
    if x < sprite_x.reinterpret_as_int() ||
      x >= sprite_x.reinterpret_as_int() + 8 {
      continue
    }
    drawn = drawn + 1
    if drawn > 8 {
      self.status.overflow = true
      break
    }
    let mut row = row
    if (attributes & 0x80) != 0 {
      row = height - 1 - row
    }
    let mut column = x - sprite_x.reinterpret_as_int()
    if (attributes & 0x40) != 0 {
      column = 7 - column
    }
    let palette = attributes & 0x03
    let tile = if self.ctrl.tall_sprites {
      // A tall sprite's index selects a 16-pixel pair: the low bit is forced to
      // zero and the row selects which of the two 8x8 tiles to use.
      let base = (tile_index & 0xFE) | (row / 8).reinterpret_as_uint()
      base
    } else {
      tile_index
    }
    let row_in_tile = (row % 8).reinterpret_as_uint()
    let bitmap = self.pattern_row(tile, row_in_tile, self.ctrl.sprite_pattern)
    let bit = 7 - column
    let pixel = (bitmap >> bit) & 3
    if pixel != 0 {
      return Some({
        colour: self.palette_byte(0x10 + palette * 4 + pixel) & 0x3F,
        is_zero: i == 0,
      })
    }
  }
  None
}

///|
/// Reads one of the PPU's eight registers.
///
/// Only `$2002` returns a value directly; the others either return the read
/// buffer's contents or return nothing a program should rely on. Register reads
/// also have side effects — reading the status clears the vblank flag and the
/// write latch — which is why they cannot be modelled as pure getters.
pub fn Ppu::read_register(self : Ppu, address : UInt32) -> UInt32 {
  match address & 0x07U {
    0x02U => {
      let mut value = 0U
      if self.status.vblank {
        value = value | 0x80U
      }
      if self.status.sprite_zero_hit {
        value = value | 0x40U
      }
      if self.status.overflow {
        value = value | 0x20U
      }
      // Reading status clears vblank and the write latch. It does not clear
      // sprite-zero hit until the pre-render line, which is what makes the flag
      // usable as a timing signal: a game polls it once and the next frame's
      // poll would otherwise see a stale set.
      self.status.vblank = false
      self.write_latch = false
      value
    }
    0x04U =>
      self.oam[self.oam_addr.reinterpret_as_int()]
      .to_int()
      .reinterpret_as_uint() &
      0xFF
    0x07U => self.read_vram()
    _ => 0U
  }
}

///|
/// Writes one of the PPU's eight registers.
pub fn Ppu::write_register(
  self : Ppu,
  address : UInt32,
  value : UInt32,
) -> Unit {
  match address & 0x07U {
    0x00U => {
      self.ctrl.nametable = value & 0x03U
      self.ctrl.increment32 = (value & 0x04) != 0
      self.ctrl.sprite_pattern = (value >> 3) & 0x01
      self.ctrl.background_pattern = (value >> 4) & 0x01
      self.ctrl.tall_sprites = (value & 0x20) != 0
      self.ctrl.nmi_enabled = (value & 0x80) != 0
      // The nametable bits live in both the control register and the scroll
      // latch; writing here updates the staged address too.
      self.t = (self.t & 0x73FF) | ((value & 0x03) << 10)
    }
    0x01U => {
      self.mask.greyscale = (value & 0x01) != 0
      self.mask.show_background_left = (value & 0x02) != 0
      self.mask.show_sprites_left = (value & 0x04) != 0
      self.mask.show_background = (value & 0x08) != 0
      self.mask.show_sprites = (value & 0x10) != 0
      self.mask.emphasize_red = (value & 0x20) != 0
      self.mask.emphasize_green = (value & 0x40) != 0
      self.mask.emphasize_blue = (value & 0x80) != 0
    }
    // $2003 sets the OAM address; it does not write OAM.
    0x03U => self.oam_addr = value & 0xFFU
    0x04U => self.write_oam(value)
    0x05U => self.write_scroll(value)
    0x06U => self.write_address(value)
    0x07U => self.write_vram(value)
    _ => ()
  }
}

///|
/// Writes a byte of OAM at the current address and advances it.
///
/// OAM is normally filled through `$4014`'s DMA rather than byte by byte, so this
/// only has to be correct for the direct-access path that a few games use.
fn Ppu::write_oam(self : Ppu, value : UInt32) -> Unit {
  let at = self.oam_addr.reinterpret_as_int() & 0xFF
  self.oam[at] = (value & 0xFF).to_byte()
  self.oam_addr = (self.oam_addr + 1U) & 0xFFU
}

///|
/// Handles a write to the scroll register `$2005`.
///
/// The first write sets the coarse and fine horizontal scroll, the second the
/// vertical. The split is invisible to the caller — it depends on a latch that
/// the status register read also clears — which is why `write_latch` is PPU state
/// and not a parameter.
fn Ppu::write_scroll(self : Ppu, value : UInt32) -> Unit {
  if !self.write_latch {
    self.t = (self.t & 0x7FE0) | ((value >> 3) & 0x1F) | ((value & 0x07) << 12)
    self.fine_x = value & 0x07
    self.write_latch = true
  } else {
    let coarse_y = (value >> 3) & 0x1F
    let fine_y = value & 0x07
    self.t = (self.t & 0x0C1F) | (coarse_y << 5) | (fine_y << 12)
    self.write_latch = false
  }
}

///|
/// Handles a write to the address register `$2006`.
fn Ppu::write_address(self : Ppu, value : UInt32) -> Unit {
  if !self.write_latch {
    self.t = (self.t & 0x00FF) | ((value & 0x3F) << 8)
    self.write_latch = true
  } else {
    self.t = (self.t & 0x7F00) | (value & 0xFF)
    self.v = self.t
    self.write_latch = false
  }
}

///|
/// Reads from the data port `$2007`.
///
/// The returned value is whatever the previous access buffered, except for
/// palette reads, which return immediately because the palette sits on the PPU's
/// own die and needs no buffering.
fn Ppu::read_vram(self : Ppu) -> UInt32 {
  let address = self.v & 0x3FFFU
  let result = if address >= 0x3F00U {
    // A palette read returns the byte immediately but still refills the buffer
    // from the mirrored nametable address beneath it.
    let mirrored = address - 0x1000U
    self.read_buffer = self.vram_read(mirrored)
    self.palette_byte(address & 0x1FU)
  } else {
    let buffered = self.read_buffer
    self.read_buffer = self.vram_read(address)
    buffered
  }
  self.v = (self.v + self.increment()) & 0x7FFFU
  result
}

///|
/// Writes to the data port `$2007`.
fn Ppu::write_vram(self : Ppu, value : UInt32) -> Unit {
  let address = self.v & 0x3FFFU
  if address >= 0x3F00U {
    self.set_palette_byte(address & 0x1FU, value)
  } else if address >= 0x2000U {
    let at = table_ram_offset(
      self.mirroring,
      (address - 0x2000U).reinterpret_as_int(),
    )
    self.nt_ram[at] = (value & 0xFF).to_byte()
  } else if self.chr_ram {
    // Only a CHR-RAM cartridge accepts writes here. On a CHR-ROM board the
    // pattern tables are read-only, and silently writing would corrupt the tile
    // bitmaps that every sprite and background fetch reads back.
    let size = self.chr.length()
    let banks = size / 0x2000
    let bank = if banks <= 0 { 0 } else { self.chr_bank.to_int() % banks }
    let at = (bank * 0x2000 + address.reinterpret_as_int() % 0x2000) % size
    self.chr[at] = (value & 0xFF).to_byte()
  }
  self.v = (self.v + self.increment()) & 0x7FFFU
}

///|
/// How far `$2007` advances the address after each access.
fn Ppu::increment(self : Ppu) -> UInt32 {
  if self.ctrl.increment32 {
    32U
  } else {
    1U
  }
}

///|
/// Reads a PPU address that is not the palette.
fn Ppu::vram_read(self : Ppu, address : UInt32) -> UInt32 {
  if address >= 0x2000U {
    let at = table_ram_offset(
      self.mirroring,
      (address - 0x2000U).reinterpret_as_int(),
    )
    self.nt_ram[at].to_int().reinterpret_as_uint() & 0xFF
  } else {
    self.chr_byte(address.reinterpret_as_int())
  }
}

///|
/// Maps a PPU address in the $2000-$3EFF range to an offset in the PPU's
/// nametable RAM.
///
/// The range is 8 KiB wide but only 2 KiB exist; the address is folded first so
/// that the cartridge's mirroring is applied to the table index, not the raw
/// offset.
fn table_ram_offset(mirroring : Mirroring, address : Int32) -> Int32 {
  let folded = address % 0x1000
  let table = folded / 0x400
  let within = folded % 0x400
  table_ram_base(mirroring, table) + within
}