1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
//! 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 {
  const BYTES_PER_TILE: usize = 64;
  const TILES_PER_BLOCK: usize = 128;
  const BLOCK_COUNT: usize = 3;
}
impl Default for DMGTileMemory {
  fn default() -> Self {
    Self {
      tiles: vec![0; Self::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::BYTES_PER_TILE;
      Some(&self.tiles[base..(base + Self::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::BYTES_PER_TILE;
      Some(&mut self.tiles[base..(base + Self::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: 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) as usize;
    let tile_y = (y / Self::PIXELS_PER_TILE) 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) as usize;
    let sub_tile_y = (y % Self::PIXELS_PER_TILE) as usize;
    tile_data[sub_tile_x + sub_tile_y * Self::PIXELS_PER_TILE 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;
    let height = DMGScreenblock::PIXELS_PER_TILE * 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 * if tall { 2 } else { 1 },
      screen_y,
      top
    );
    let index = (if tall {
      if local_y < DMGScreenblock::PIXELS_PER_TILE {
        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 {
      local_y - DMGScreenblock::PIXELS_PER_TILE
    } else {
      local_y
    };
    //dump!(local_x, local_y);
    tile_data[(local_x + local_y * DMGScreenblock::PIXELS_PER_TILE) 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]);
/// ```
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: &[u32; 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 {
        0xFF_0000FF
      } else if screen_y as isize == 144 {
        0xFF_00FF00
      } else {
        out_palette[final_index as usize]
      };
      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: &[u32; 4],
  mut hblank: H,
) {
  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 {
        0xFF_0000FF
      } else if screen_y as isize == 144 {
        0xFF_00FF00
      } else {
        out_palette[final_index as usize]
      };
      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);
  }
}