thorium 0.4.0

Lokathor does stuff, ium
Documentation
//! Module for rendering a Game Boy style image.

#![allow(dead_code)]

use super::*;

/// Holds a complete configuration for generating a DMG image (memory is elsewhere).
#[derive(Debug, Clone)]
pub struct DMGConfig {
  /// Starting X (within BG space) of the viewport.
  pub viewport_scroll_x: u8,
  /// Starting Y (within BG space) of the viewport.
  pub viewport_scroll_y: u8,
  /// Adjusts how the background and window index into the tile memory.
  ///
  /// * false: values are unsigned, starting from block 0, addressing into 0/1.
  /// * true: values are signed, starting from block 2, addressing into 1/2.
  pub bg_win_use_signed_addressing: bool,
  /// If the background should display according to the low screenblock or the
  /// high screenblock
  pub background_uses_high_screenblock: bool,
  /// If the window should display according to the low screenblock or the high
  /// screenblock
  pub window_uses_high_screenblock: bool,
  /// If objects should display as 8x16 (tall) instead of 8x8 (square)
  pub objects_are_tall: bool,
  /// If objects should be displayed at all.
  pub objects_enabled: bool,
  /// If the window should be displayed at all.
  pub window_enabled: bool,
  /// If the background and window should be displayed at all.
  ///
  /// If this is false then `window_enabled` is ignored and only objects are
  /// displayed (assuming they're enabled).
  pub bg_win_enabled: bool,
  /// Y position of the upper left corner of the window (viewport relative).
  ///
  /// Use 0 to align the window's top with the viewport top.
  pub window_y: u8,
  /// X position (+7) of the upper left corner of the window (viewport
  /// relative).
  ///
  /// Use 7 to align the window's left with the viewport left. Values of 0-6
  /// were buggy on real hardware.
  pub window_xp7: u8,
  /// Translates background and window entries into palette index values.
  pub bg_win_palette: [u8; 4],
  /// Translates object entries into palette index values (but 0 is always
  /// transparent).
  ///
  /// Individual objects can use either the low or high palette.
  pub object_palette: [[u8; 4]; 2],
  /// The limit of how many objects to show on a given scanline.
  ///
  /// The real limit is 10, but it might as well be adjustable.
  pub max_objects_per_scanline: usize,
}
impl Default for DMGConfig {
  fn default() -> Self {
    Self {
      viewport_scroll_x: 0,
      viewport_scroll_y: 0,
      bg_win_use_signed_addressing: false,
      background_uses_high_screenblock: false,
      window_uses_high_screenblock: true,
      objects_are_tall: false,
      objects_enabled: false,
      window_enabled: false,
      bg_win_enabled: true,
      window_y: 0,
      window_xp7: 7,
      bg_win_palette: [0, 1, 2, 3],
      object_palette: [[0, 1, 2, 3], [0, 1, 2, 3]],
      max_objects_per_scanline: 10,
    }
  }
}

/// Stores tile data in index format.
#[derive(Debug, Clone)]
pub struct DMGTileMemory {
  /// Index data for the tiles
  pub tiles: Vec<u8>,
}
impl DMGTileMemory {
  /// Bytes in a compressed tile (2bpp)
  pub const COMPRESSED_BYTES_PER_TILE: usize = 16;
  /// Bytes once a tile is inflated (8bpp)
  pub const INFLATED_BYTES_PER_TILE: usize = 64;
  /// Tiles in a tile block
  pub const TILES_PER_BLOCK: usize = 128;
  /// Number of tile blocks in the memory.
  pub const BLOCK_COUNT: usize = 3;
}
impl Default for DMGTileMemory {
  fn default() -> Self {
    Self {
      tiles: vec![0; Self::INFLATED_BYTES_PER_TILE * Self::TILES_PER_BLOCK * Self::BLOCK_COUNT],
    }
  }
}
impl DMGTileMemory {
  /// Gets the indexes of the tile specified (if it's in bounds).
  pub fn get_tile(&self, slot: usize) -> Option<&[u8]> {
    if slot < Self::TILES_PER_BLOCK * Self::BLOCK_COUNT {
      let base = slot * Self::INFLATED_BYTES_PER_TILE;
      Some(&self.tiles[base..(base + Self::INFLATED_BYTES_PER_TILE)])
    } else {
      None
    }
  }
  /// Gets the indexes of the tile specified (if it's in bounds).
  pub fn get_tile_mut(&mut self, slot: usize) -> Option<&mut [u8]> {
    if slot < Self::TILES_PER_BLOCK * Self::BLOCK_COUNT {
      let base = slot * Self::INFLATED_BYTES_PER_TILE;
      Some(&mut self.tiles[base..(base + Self::INFLATED_BYTES_PER_TILE)])
    } else {
      None
    }
  }
}

/// Stores data for the background and/or window layout
pub struct DMGScreenblock {
  /// Indexes into the tile memory (details depend on the config).
  pub entries: Vec<u8>,
}
impl DMGScreenblock {
  const PIXELS_PER_TILE_SIDE: u8 = 8;
  const TILES_PER_ROW: usize = 32;
  const ROW_COUNT: usize = 32;

  /// Gets the requested pixel out of the screenblock.
  ///
  /// Because `x` and `y` are limited to the `u8` range, this will never panic.
  pub fn get_pixel(&self, x: u8, y: u8, memory: &DMGTileMemory, use_signed: bool) -> u8 {
    let tile_x = (x / Self::PIXELS_PER_TILE_SIDE) as usize;
    let tile_y = (y / Self::PIXELS_PER_TILE_SIDE) as usize;
    let entry_value = self.entries[tile_x + tile_y * Self::TILES_PER_ROW];
    let tile_data = if use_signed {
      let signed_value = entry_value as i8;
      let index = ((2 * DMGTileMemory::TILES_PER_BLOCK) as isize + signed_value as isize) as usize;
      memory.get_tile(index)
    } else {
      memory.get_tile(entry_value as usize)
    }
    .unwrap();
    let sub_tile_x = (x % Self::PIXELS_PER_TILE_SIDE) as usize;
    let sub_tile_y = (y % Self::PIXELS_PER_TILE_SIDE) as usize;
    tile_data[sub_tile_x + sub_tile_y * Self::PIXELS_PER_TILE_SIDE as usize]
  }
}
impl Default for DMGScreenblock {
  fn default() -> Self {
    Self {
      entries: vec![0; Self::TILES_PER_ROW * Self::ROW_COUNT],
    }
  }
}

/// An individual entry within the DMG OAM.
#[derive(Debug, Clone, Default)]
pub struct DMGOAMEntry {
  /// X-8 of the object (viewport relative).
  ///
  /// Objects are 8 pixels wide, so if you use 0 here then the object will be
  /// all the way off the left of the screen.
  pub xm8: u8,
  /// Y-16 of the object (viewport relative).
  ///
  /// Objects are 8 or 16 pixels tall, so if this value is low enough the object
  /// will be all the way above the screen.
  pub ym16: u8,
  /// The tile index for this object.
  ///
  /// Objects always index from the base of block 0 of the VRAM. If objects are
  /// set for "tall" mode, then the least bit is ignored and the upper part of
  /// the object will be the even index of the tile pair (`tile_id & 0xFE`) and
  /// the lower part of the object will be the odd index (`tile_id | 0x01`).
  pub tile_id: u8,
  /// Causes this object to be "behind" non-zero-index background/win pixels.
  ///
  /// If this flag is _enabled_ wherever the pixel from the background or window
  /// comes from indexes 1, 2, or 3 then the object won't get to use its pixel.
  /// If this flag is disabled then the object overrides the base index all the
  /// time.
  pub display_behind_bg_win_13: bool,
  /// Flip the object vertically.
  pub vflip: bool,
  /// Flip the object horizontally.
  pub hflip: bool,
  /// If the object should use the high bank for palette translation.
  pub use_high_palette: bool,
}
impl DMGOAMEntry {
  /// Obtains the left and top position of the object in screen space.
  pub fn screen_left_top(&self) -> (u8, u8) {
    (self.xm8.wrapping_sub(8), self.ym16.wrapping_sub(16))
  }
  /// Obtains the pixel from this object on the screen.
  ///
  /// # Panics
  ///
  /// If the screen_x or screen_y are out of bounds this will panic
  pub fn get_pixel(&self, screen_x: u8, screen_y: u8, memory: &DMGTileMemory, tall: bool) -> u8 {
    let (left, top) = self.screen_left_top();
    let width = DMGScreenblock::PIXELS_PER_TILE_SIDE;
    let height = DMGScreenblock::PIXELS_PER_TILE_SIDE * if tall { 2 } else { 1 };
    let local_x = if self.hflip {
      width - (screen_x.wrapping_sub(left) + 1)
    } else {
      screen_x.wrapping_sub(left)
    };
    let local_y = if self.vflip {
      height - (screen_y.wrapping_sub(top) + 1)
    } else {
      screen_y.wrapping_sub(top)
    };
    debug_assert!(local_x < width, "local_x:{}", local_x);
    debug_assert!(
      local_y < height,
      "local_y:{}, limit:{}, screen_y:{}, screen_top:{}",
      local_y,
      DMGScreenblock::PIXELS_PER_TILE_SIDE * if tall { 2 } else { 1 },
      screen_y,
      top
    );
    let index = (if tall {
      if local_y < DMGScreenblock::PIXELS_PER_TILE_SIDE {
        self.tile_id & 0xFE
      } else {
        self.tile_id | 0x01
      }
    } else {
      self.tile_id
    }) as usize;
    let tile_data = memory.get_tile(index).unwrap();
    let local_y = if tall && local_y >= DMGScreenblock::PIXELS_PER_TILE_SIDE {
      local_y - DMGScreenblock::PIXELS_PER_TILE_SIDE
    } else {
      local_y
    };
    //dump!(local_x, local_y);
    tile_data[(local_x + local_y * DMGScreenblock::PIXELS_PER_TILE_SIDE) as usize]
  }
}

/// Inflates DMG `(low, high)` image bytes into standard index values.
///
/// ```
/// use thorium::dmg_render::*;
/// assert_eq!(dmg_inflate_row(0xFF, 0), [1u8; 8]);
/// assert_eq!(dmg_inflate_row(0, 0xFF), [2u8; 8]);
/// assert_eq!(dmg_inflate_row(0x7C, 0x7C), [0,3,3,3,3,3,0,0]);
/// ```
#[allow(clippy::identity_op)]
pub const fn dmg_inflate_row(low: u8, high: u8) -> [u8; 8] {
  let mut out = [0; 8];
  out[7] = (((1 << 0) & low) > 0) as u8 | (((((1 << 0) & high) > 0) as u8) << 1);
  out[6] = (((1 << 1) & low) > 0) as u8 | (((((1 << 1) & high) > 0) as u8) << 1);
  out[5] = (((1 << 2) & low) > 0) as u8 | (((((1 << 2) & high) > 0) as u8) << 1);
  out[4] = (((1 << 3) & low) > 0) as u8 | (((((1 << 3) & high) > 0) as u8) << 1);
  out[3] = (((1 << 4) & low) > 0) as u8 | (((((1 << 4) & high) > 0) as u8) << 1);
  out[2] = (((1 << 5) & low) > 0) as u8 | (((((1 << 5) & high) > 0) as u8) << 1);
  out[1] = (((1 << 6) & low) > 0) as u8 | (((((1 << 6) & high) > 0) as u8) << 1);
  out[0] = (((1 << 7) & low) > 0) as u8 | (((((1 << 7) & high) > 0) as u8) << 1);
  out
}

/// Writes inflated index values to `dest` using DMG image bytes in `src.
///
/// Keeps going until either side doesn't have space left. Partial rows at the
/// end are not processed.
/// ```
/// use thorium::dmg_render::*;
/// let mut src = [0xFF, 0, 0, 0xFF];
/// let mut dest = [0; 16];
/// dmg_inflate_tile_data(&mut dest, &src);
/// assert_eq!(&dest[0..8], &[1u8; 8]);
/// assert_eq!(&dest[8..16], &[2u8; 8]);
/// ```
pub fn dmg_inflate_tile_data(dest: &mut [u8], src: &[u8]) {
  for (dest_row, source_row) in dest.chunks_exact_mut(8).zip(src.chunks_exact(2)) {
    unsafe {
      dest_row.copy_from_slice(&dmg_inflate_row(
        *source_row.get_unchecked(0),
        *source_row.get_unchecked(1),
      ));
    }
  }
}

/// renders out the first 256 tiles in the tile memory into a plain grid
#[allow(clippy::cast_ptr_alignment)]
pub fn render_tile_memory(bitmap: &mut Bitmap, tile_memory: &DMGTileMemory) {
  for tile_id in 0..256 {
    let slice = tile_memory
      .get_tile(tile_id)
      .expect("why don't we have at least 256 tiles?");
    let tiles_per_row = 16;
    let tile_base_x = (tile_id % tiles_per_row) as isize;
    let tile_base_y = (tile_id / tiles_per_row) as isize;
    let bytes_per_pixel = 4;
    let pixels_per_tile = 8;
    let tile_base_ptr = unsafe {
      bitmap
        .memory
        .offset(pixels_per_tile * bytes_per_pixel * tile_base_x)
        .offset(pixels_per_tile * bitmap.pitch * tile_base_y)
    };
    for (row_dy, slice_row) in slice.chunks_exact(8).enumerate() {
      let mut row_ptr = unsafe { tile_base_ptr.offset(row_dy as isize * bitmap.pitch) as *mut u32 };
      for (_row_dx, byte) in slice_row.iter().enumerate() {
        let color: u32 = match byte {
          0 => 0xFF_ffffb5,
          1 => 0xFF_7bc67b,
          2 => 0xFF_6b8c42,
          3 => 0xFF_5a3921,
          z => unimplemented!("unexpected byte index value! {}", z),
        };
        unsafe {
          row_ptr.write_unaligned(color);
          row_ptr = row_ptr.offset(1)
        };
      }
    }
  }
}

/// Displays an entire screenblock into a Bitmap.
#[allow(clippy::cast_ptr_alignment)]
pub fn render_screen_block(
  bitmap: &mut Bitmap, tile_memory: &DMGTileMemory, screenblock: &DMGScreenblock, scroll_x: u8,
  scroll_y: u8,
) {
  const TILES_PER_ROW: usize = 32;
  const PIXELS_PER_TILE: usize = 8;
  assert!(bitmap.width as usize >= PIXELS_PER_TILE * TILES_PER_ROW);
  assert!(bitmap.height as usize >= PIXELS_PER_TILE * TILES_PER_ROW);
  let mut row_ptr = bitmap.memory;
  for screen_y in 0..(PIXELS_PER_TILE * TILES_PER_ROW) {
    let mut pixel_ptr = row_ptr as *mut u32;
    for screen_x in 0..(PIXELS_PER_TILE * TILES_PER_ROW) {
      let screenblock_pixel_x: u8 = (screen_x as u8).wrapping_add(scroll_x);
      let screenblock_pixel_y: u8 = (screen_y as u8).wrapping_add(scroll_y);
      let screenblock_grid_x: usize = screenblock_pixel_x as usize / PIXELS_PER_TILE;
      let screenblock_grid_y: usize = screenblock_pixel_y as usize / PIXELS_PER_TILE;
      let screenblock_index: usize = screenblock_grid_x + screenblock_grid_y * TILES_PER_ROW;
      let tile_memory_index: u8 = *screenblock.entries.get(screenblock_index).unwrap();
      let tile_data: &[u8] = tile_memory.get_tile(tile_memory_index as usize).unwrap();
      let tile_pixel_x: usize = screenblock_pixel_x as usize % PIXELS_PER_TILE;
      let tile_pixel_y: usize = screenblock_pixel_y as usize % PIXELS_PER_TILE;
      let pal_index: u8 = *tile_data
        .get(tile_pixel_y * PIXELS_PER_TILE + tile_pixel_x)
        .unwrap();
      let color: u32 = if screen_x as isize == 160 {
        0xFF_0000FF
      } else if screen_y as isize == 144 {
        0xFF_00FF00
      } else {
        match pal_index {
          0 => 0xFF_ffffb5,
          1 => 0xFF_7bc67b,
          2 => 0xFF_6b8c42,
          3 => 0xFF_5a3921,
          z => unimplemented!("unexpected byte index value! {}", z),
        }
      };
      unsafe {
        pixel_ptr.write_unaligned(color);
        pixel_ptr = pixel_ptr.offset(1);
      }
    }
    row_ptr = unsafe { row_ptr.offset(bitmap.pitch) };
  }
}

/// Performs a basic DMG style rendering.
///
/// The term "basic" here means that there's no chance to change any state
/// during what would be the "HBlank" periods.
#[allow(clippy::cast_ptr_alignment)]
pub fn dmg_basic_render(
  bitmap: &mut Bitmap, config: &DMGConfig, tile_memory: &DMGTileMemory,
  screenblocks: &[&DMGScreenblock], oam: &[DMGOAMEntry], out_palette: &[ARGB32; 4],
) {
  const TILES_PER_ROW: usize = 32;
  const PIXELS_PER_TILE: usize = 8;
  assert!(bitmap.width as usize >= PIXELS_PER_TILE * TILES_PER_ROW);
  assert!(bitmap.height as usize >= PIXELS_PER_TILE * TILES_PER_ROW);
  let mut row_ptr = bitmap.memory;
  for screen_y in 0..(PIXELS_PER_TILE * TILES_PER_ROW) {
    let mut oam_this_row: Vec<&DMGOAMEntry> = if config.objects_enabled {
      oam
        .iter()
        .filter(|entry| {
          let (_x, y) = entry.screen_left_top();
          let top = y as usize;
          let bottom = top + if config.objects_are_tall { 16 } else { 8 };
          screen_y >= top as usize && screen_y < bottom
        })
        .take(config.max_objects_per_scanline)
        .collect()
    } else {
      vec![]
    };
    // Note: this is a stable sort.
    oam_this_row.sort_by_key(|entry| entry.xm8);
    let mut pixel_ptr = row_ptr as *mut u32;
    for screen_x in 0..(PIXELS_PER_TILE * TILES_PER_ROW) {
      let bg_win_pal_index: Option<u8> = if config.bg_win_enabled {
        Some(
          if config.window_enabled
            && screen_y >= config.window_y as usize
            && screen_x >= config.window_xp7.wrapping_sub(7) as usize
          {
            let win_x = screen_x as u8 - config.window_xp7.wrapping_sub(7);
            let win_y = screen_y as u8 - config.window_y;
            screenblocks[config.window_uses_high_screenblock as usize].get_pixel(
              win_x,
              win_y,
              tile_memory,
              config.bg_win_use_signed_addressing,
            )
          } else {
            let bg_x: u8 = (screen_x as u8).wrapping_add(config.viewport_scroll_x);
            let bg_y: u8 = (screen_y as u8).wrapping_add(config.viewport_scroll_y);
            screenblocks[config.background_uses_high_screenblock as usize].get_pixel(
              bg_x,
              bg_y,
              tile_memory,
              config.bg_win_use_signed_addressing,
            )
          },
        )
      } else {
        None
      };
      let obj_index: Option<(u8, bool, bool)> = if config.objects_enabled {
        oam_this_row
          .iter()
          .find(|entry| {
            let (left, _top) = entry.screen_left_top();
            screen_x >= left as usize && screen_x < (left + 8) as usize
          })
          .map(|entry| {
            (
              entry.get_pixel(
                screen_x as u8,
                screen_y as u8,
                tile_memory,
                config.objects_are_tall,
              ),
              entry.display_behind_bg_win_13,
              entry.use_high_palette,
            )
          })
      } else {
        None
      };
      let final_index = match (bg_win_pal_index, obj_index) {
        (None, None) => 0,
        (Some(bg_i), None) => config.bg_win_palette[bg_i as usize],
        (None, Some((obj_i, _behind, high))) => {
          config.object_palette[high as usize][obj_i as usize]
        }
        (Some(bg_i), Some((obj_i, behind, high))) => {
          if obj_i == 0 || behind && bg_i > 0 {
            config.bg_win_palette[bg_i as usize]
          } else {
            config.object_palette[high as usize][obj_i as usize]
          }
        }
      };
      let color: u32 = if screen_x as isize == 160 || screen_x as isize == 240 {
        0xFF_0000FF
      } else if screen_y as isize == 144 || screen_y as isize == 160 {
        0xFF_00FF00
      } else {
        out_palette[final_index as usize].into()
      };
      unsafe {
        pixel_ptr.write_unaligned(color);
        pixel_ptr = pixel_ptr.offset(1);
      }
    }
    row_ptr = unsafe { row_ptr.offset(bitmap.pitch) };
  }
}

/// Performs a DMG style rendering with hblank support.
///
/// Renders a single scanline, then calls the hblank function which is allowed
/// to do basically anything it wants.
#[allow(clippy::cast_ptr_alignment)]
pub fn dmg_hblank_render<
  H: FnMut(&mut DMGConfig, &mut DMGTileMemory, &mut [&mut DMGScreenblock], &mut [DMGOAMEntry]),
>(
  bitmap: &mut Bitmap, config: &mut DMGConfig, tile_memory: &mut DMGTileMemory,
  screenblocks: &mut [&mut DMGScreenblock], oam: &mut [DMGOAMEntry], out_palette: &[ARGB32; 4],
  mut hblank: H,
) {
  const TILES_PER_ROW: usize = DMGScreenblock::TILES_PER_ROW as usize;
  const PIXELS_PER_TILE_SIDE: usize = DMGScreenblock::PIXELS_PER_TILE_SIDE as usize;
  assert!(bitmap.width as usize >= PIXELS_PER_TILE_SIDE * TILES_PER_ROW);
  assert!(bitmap.height as usize >= PIXELS_PER_TILE_SIDE * TILES_PER_ROW);
  let mut row_ptr = bitmap.memory;
  for screen_y in 0..(PIXELS_PER_TILE_SIDE * TILES_PER_ROW) {
    let mut oam_this_row: Vec<&DMGOAMEntry> = if config.objects_enabled {
      oam
        .iter()
        .filter(|entry| {
          // TODO: this is basically "contains" but terrible, make it less terrible.
          let (_x, y) = entry.screen_left_top();
          for dy in 0..if config.objects_are_tall { 16 } else { 8 } {
            let augmented_y = y.wrapping_add(dy) as usize;
            if augmented_y == screen_y {
              return true;
            }
          }
          false
        })
        .take(config.max_objects_per_scanline)
        .collect()
    } else {
      vec![]
    };
    // Note: this is a stable sort.
    oam_this_row.sort_by_key(|entry| entry.xm8);
    let mut pixel_ptr = row_ptr as *mut u32;
    for screen_x in 0..(PIXELS_PER_TILE_SIDE * TILES_PER_ROW) {
      let bg_win_pal_index: Option<u8> = if config.bg_win_enabled {
        Some(
          if config.window_enabled
            && screen_y >= config.window_y as usize
            && screen_x >= config.window_xp7.wrapping_sub(7) as usize
          {
            let win_x = screen_x as u8 - config.window_xp7.wrapping_sub(7);
            let win_y = screen_y as u8 - config.window_y;
            screenblocks[config.window_uses_high_screenblock as usize].get_pixel(
              win_x,
              win_y,
              tile_memory,
              config.bg_win_use_signed_addressing,
            )
          } else {
            let bg_x: u8 = (screen_x as u8).wrapping_add(config.viewport_scroll_x);
            let bg_y: u8 = (screen_y as u8).wrapping_add(config.viewport_scroll_y);
            screenblocks[config.background_uses_high_screenblock as usize].get_pixel(
              bg_x,
              bg_y,
              tile_memory,
              config.bg_win_use_signed_addressing,
            )
          },
        )
      } else {
        None
      };
      let obj_index: Option<(u8, bool, bool)> = if config.objects_enabled {
        oam_this_row
          .iter()
          .find(|entry| {
            let (left, _top) = entry.screen_left_top();
            for dx in 0..8 {
              let augmented_x = left.wrapping_add(dx) as usize;
              if augmented_x == screen_x {
                return true;
              }
            }
            false
          })
          .map(|entry| {
            (
              entry.get_pixel(
                screen_x as u8,
                screen_y as u8,
                tile_memory,
                config.objects_are_tall,
              ),
              entry.display_behind_bg_win_13,
              entry.use_high_palette,
            )
          })
      } else {
        None
      };
      let final_index = match (bg_win_pal_index, obj_index) {
        (None, None) => 0,
        (Some(bg_i), None) => config.bg_win_palette[bg_i as usize],
        (None, Some((obj_i, _behind, high))) => {
          config.object_palette[high as usize][obj_i as usize]
        }
        (Some(bg_i), Some((obj_i, behind, high))) => {
          if obj_i == 0 || behind && bg_i > 0 {
            config.bg_win_palette[bg_i as usize]
          } else {
            config.object_palette[high as usize][obj_i as usize]
          }
        }
      };
      let color: u32 = if screen_x as isize == 160 || screen_x as isize == 240 {
        0xFF_0000FF
      } else if screen_y as isize == 144 || screen_y as isize == 160 {
        0xFF_00FF00
      } else {
        out_palette[final_index as usize].into()
      };
      unsafe {
        pixel_ptr.write_unaligned(color);
        pixel_ptr = pixel_ptr.offset(1);
      }
    }
    row_ptr = unsafe { row_ptr.offset(bitmap.pitch) };
    hblank(config, tile_memory, screenblocks, oam);
  }
}