Skip to main content

gb/ppu/
tile.rs

1//! Tile data: the 384 slots at `0x8000`, sixteen bytes each.
2//!
3//! # Slots and indices
4//!
5//! A tilemap byte is eight bits, so it names one of 256 tiles, but there are 384.
6//! `LCDC` bit 4 chooses which window of 256 it names, and the two windows overlap
7//! in the middle:
8//!
9//! | Block | Address | Slot | [`Base8000`](Addressing::Base8000) | [`Base8800`](Addressing::Base8800) |
10//! |---|---|---|---|---|
11//! | 0 | `8000-87FF` | 0-127 | index 0-127 | out of reach |
12//! | 1 | `8800-8FFF` | 128-255 | index 128-255 | index 128-255 |
13//! | 2 | `9000-97FF` | 256-383 | out of reach | index 0-127 |
14//!
15//! Objects ignore the bit and always read as `Base8000`, so putting the
16//! background on `Base8800` gives each a private block and leaves block 1
17//! shared, putting all 384 within reach at once.
18//!
19//! Writing here addresses a slot, which means the same tile whichever mode is
20//! set; [`index`] converts one to the byte a map needs. See
21//! <https://gbdev.io/pandocs/Tile_Data.html>.
22
23use crate::mmio::{LCDC, Tile, VRAM_TILES};
24
25use super::Access;
26
27/// Tile slots in one VRAM bank.
28pub const SLOT_COUNT: u16 = 384;
29
30/// Which 256 slots a tilemap byte names, from `LCDC` bit 4.
31///
32/// The `$8000` and `$8800` methods, named as Pan Docs and the wider Game Boy
33/// development community name them. `Base8800` is a misnomer kept for that
34/// familiarity: its base pointer is `0x9000`, and the index is read as signed,
35/// which is what puts its second half back down in block 1.
36#[derive(Clone, Copy, PartialEq, Eq, Debug)]
37pub enum Addressing {
38    /// Base `0x8000`, index unsigned: blocks 0 and 1. Objects always use this.
39    Base8000,
40    /// Base `0x9000`, index signed: blocks 2 and 1.
41    Base8800,
42}
43
44/// The slot a tilemap byte of `index` names under `mode`.
45pub const fn slot(mode: Addressing, index: u8) -> u16 {
46    match mode {
47        Addressing::Base8000 => index as u16,
48        // Signed: 0..=127 land in block 2, and 128..=255 read as -128..=-1 and
49        // fall back into block 1.
50        Addressing::Base8800 if index < 128 => 256 + index as u16,
51        Addressing::Base8800 => index as u16,
52    }
53}
54
55/// The tilemap byte that names `slot` under `mode`, if `mode` reaches it.
56///
57/// `None` for the block the mode leaves out, and for a slot past [`SLOT_COUNT`].
58pub const fn index(mode: Addressing, slot: u16) -> Option<u8> {
59    match mode {
60        Addressing::Base8000 if slot < 256 => Some(slot as u8),
61        Addressing::Base8800 if slot >= 256 && slot < SLOT_COUNT => Some((slot - 256) as u8),
62        Addressing::Base8800 if slot >= 128 && slot < 256 => Some(slot as u8),
63        _ => None,
64    }
65}
66
67/// Which window `LCDC` currently selects for the background and window layers.
68#[inline]
69pub fn addressing() -> Addressing {
70    if LCDC.read().tiledata_8000() {
71        Addressing::Base8000
72    } else {
73        Addressing::Base8800
74    }
75}
76
77/// Select the window for the background and window layers.
78#[inline]
79pub fn set_addressing(mode: Addressing) {
80    let on = mode == Addressing::Base8000;
81    // Read-modify-write: the enable bit is the only reason this is unsafe.
82    unsafe { LCDC.write(LCDC.read().with_tiledata_8000(on)) };
83}
84
85/// Write one tile into `slot`.
86///
87/// # Panics
88///
89/// If `slot` is [`SLOT_COUNT`] or beyond, which would otherwise land in a
90/// tilemap. The check folds away when `slot` is a constant.
91#[inline]
92pub fn write(access: Access<'_>, slot: u16, data: &Tile) {
93    let dst = VRAM_TILES.index(slot as usize).as_usize() as *mut u8;
94    unsafe { access.write(dst, data) };
95}
96
97/// Write `data` into consecutive slots from `first`.
98///
99/// # Panics
100///
101/// If the run would reach [`SLOT_COUNT`].
102pub fn write_all(access: Access<'_>, first: u16, data: &[Tile]) {
103    assert!(
104        (first as usize)
105            .checked_add(data.len())
106            .is_some_and(|end| end <= SLOT_COUNT as usize)
107    );
108    if data.is_empty() {
109        return;
110    }
111    // Slots are contiguous, so the whole run is one copy.
112    let dst = VRAM_TILES.index(first as usize).as_usize() as *mut u8;
113    unsafe { access.write(dst, data.as_flattened()) };
114}