Skip to main content

gb/ppu/
obj.rs

1//! Objects: the 40 sprites the PPU draws over the background.
2//!
3//! Each is four bytes in OAM at `0xFE00`, described by [`OamEntry`]. Position is
4//! offset so that a sprite can sit partly off screen: `y` of 16 and `x` of 8 put
5//! it at the top left corner, and either axis at zero hides it.
6//!
7//! Object tiles always read as [`Base8000`](super::tile::Addressing::Base8000),
8//! whatever the background is set to. In [`Tall`](Size::Tall) the low bit of the
9//! index is ignored and the pair is drawn as one 8 by 16 sprite. See
10//! <https://gbdev.io/pandocs/OAM.html>.
11//!
12//! # What the hardware drops
13//!
14//! Only [`PER_SCANLINE`] objects are drawn on any one scanline; the rest of that
15//! line's are skipped. Which ones survive is by `x` on the original Game Boy, lower first,
16//! and by OAM index on the Game Boy Color. Nothing reports the loss.
17//!
18//! # Writing OAM
19//!
20//! Writing entries directly costs a blanking window per byte. The usual way is a
21//! [`OamShadow`] page in work RAM, edited whenever, handed to the hardware in one
22//! [`OamDma`] once a frame. See
23//! <https://gbdev.io/pandocs/OAM_DMA_Transfer.html>.
24
25use gb_hram::HramArea;
26use gb_ram_fn::{RamFn, ram_fn};
27
28use crate::interrupt::CriticalSection;
29use crate::mmio::{LCDC, OAM, OamAttr, OamEntry};
30
31use super::Access;
32
33/// Objects OAM holds.
34pub const ENTRY_COUNT: u8 = 40;
35
36/// Objects the PPU draws on one scanline.
37pub const PER_SCANLINE: u8 = 10;
38
39/// How tall an object is, from `LCDC` bit 2. It applies to all of them at once.
40#[derive(Clone, Copy, PartialEq, Eq, Debug)]
41pub enum Size {
42    /// 8 by 8.
43    Small,
44    /// 8 by 16: the tile index's low bit is ignored and the pair is drawn.
45    Tall,
46}
47
48/// Write one entry.
49///
50/// # Panics
51///
52/// If `i` is [`ENTRY_COUNT`] or beyond.
53#[inline]
54pub fn set(access: Access<'_>, i: u8, entry: OamEntry) {
55    let dst = OAM.index(i as usize).as_usize() as *mut u8;
56    let bytes = [entry.y, entry.x, entry.tile, u8::from(entry.attr)];
57    unsafe { access.write(dst, &bytes) };
58}
59
60/// Move an object off screen, which is the only way to stop it being drawn.
61///
62/// There is no per-object enable bit; [`set_enabled`] switches all forty at once.
63/// Writing zero to `y` puts the object sixteen pixels above the top edge, and
64/// that one byte is the whole of it: `x`, the tile and the attributes can stay.
65///
66/// [`OamShadow::new`] leaves every object hidden this way, so a page starts
67/// empty rather than needing to be cleared.
68///
69/// # Panics
70///
71/// If `i` is [`ENTRY_COUNT`] or beyond.
72#[inline]
73pub fn hide(access: Access<'_>, i: u8) {
74    let dst = OAM.index(i as usize).as_usize() as *mut u8;
75    unsafe { access.write(dst, &[0]) };
76}
77
78/// Whether the PPU draws objects at all, from `LCDC` bit 1.
79#[inline]
80pub fn enabled() -> bool {
81    LCDC.read().obj_enable()
82}
83
84/// Draw objects, or stop.
85#[inline]
86pub fn set_enabled(on: bool) {
87    // Read-modify-write: the enable bit is the only reason this is unsafe.
88    unsafe { LCDC.write(LCDC.read().with_obj_enable(on)) };
89}
90
91/// The size every object is drawn at.
92#[inline]
93pub fn size() -> Size {
94    if LCDC.read().obj_tall() { Size::Tall } else { Size::Small }
95}
96
97/// Set the size every object is drawn at.
98#[inline]
99pub fn set_size(size: Size) {
100    unsafe { LCDC.write(LCDC.read().with_obj_tall(size == Size::Tall)) };
101}
102
103/// Bytes past the entries that the alignment pays for anyway.
104pub const SPARE: usize = 256 - ENTRY_COUNT as usize * core::mem::size_of::<OamEntry>();
105
106/// A page of entries for [`OamDma`] to hand over.
107///
108/// The hardware takes only the high byte of the source address, so this is
109/// aligned to 256. Anywhere the linker puts a `static` is a legal source: work
110/// RAM runs `0xC000..0xE000` and read-only data sits below `0x8000`, both inside
111/// the `0x0000..0xE000` the transfer can read.
112///
113/// Alignment rounds the size up from the 160 bytes of entries, and
114/// [`spare`](Self::spare) is the remainder. A transfer reads the entries only, so
115/// those bytes are free for whatever wants to travel with the objects: per-object
116/// velocities, animation counters, what a metasprite each belongs to.
117#[repr(align(256))]
118pub struct OamShadow {
119    /// What the transfer hands to the hardware.
120    pub entries: [OamEntry; ENTRY_COUNT as usize],
121    /// Room the alignment leaves over. The transfer does not read it.
122    pub spare: [u8; SPARE],
123}
124
125// Catches the entries and the spare drifting out of step with the alignment.
126const _: () = assert!(core::mem::size_of::<OamShadow>() == 256);
127
128impl OamShadow {
129    /// A page with every object hidden and the spare zeroed.
130    pub const fn new() -> Self {
131        OamShadow {
132            entries: [OamEntry { y: 0, x: 0, tile: 0, attr: OamAttr::new() }; ENTRY_COUNT as usize],
133            spare: [0; SPARE],
134        }
135    }
136}
137
138impl Default for OamShadow {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144/// Bytes [`OamDma::install`] needs. Keep in step with the `ram_fn` below.
145pub const DMA_LEN: usize = 8;
146
147// The CPU may touch nothing but HRAM while the transfer runs, so the code that
148// starts it and waits it out has to be running from there. `install` copies this.
149#[ram_fn(max = 8)]
150fn dma_routine(page: u8) {
151    unsafe {
152        core::arch::asm!(
153            "ldh ($46), a",
154            "ld a, 40",
155            "2:",
156            "dec a",
157            "jr nz, 2b",
158            inout("a") page => _,
159            options(nostack),
160        );
161    }
162}
163
164/// The transfer routine, once it is in HRAM and callable.
165///
166/// Holding one is the proof that the copy has happened, so [`run`](Self::run)
167/// needs no check and cannot be reached before it.
168pub struct OamDma(<dma_routine::Handle as RamFn>::Fn);
169
170// A bare `fn` is not `BankSafe` because it may point into a bank a switch would
171// unmap. This one is installed into HRAM, which no switch touches.
172#[cfg(feature = "bank")]
173unsafe impl crate::bank::BankSafe for OamDma {}
174
175impl OamDma {
176    /// Copy the routine into `buf` and return the handle.
177    ///
178    /// Call once. Doing it again is harmless but copies the bytes over
179    /// themselves.
180    pub fn install(buf: &'static HramArea<DMA_LEN>) -> Self {
181        OamDma(unsafe { dma_routine.install(buf.as_mut_ptr() as *mut [u8; DMA_LEN]) })
182    }
183
184    /// Hand `src` to the hardware, then wait out the 160 M-cycle transfer.
185    ///
186    /// The [`CriticalSection`] is required: an interrupt during the transfer
187    /// would send the CPU to a vector it cannot read.
188    ///
189    /// The PPU cannot read OAM meanwhile either, so a transfer reaching over a
190    /// visible line leaves that line's objects undrawn.
191    /// [`Polled`](Access::Polled) waits for a VBlank with room for the whole of
192    /// it; [`Direct`](Access::Direct) starts at once, as a VBlank handler and a
193    /// deliberate mid-frame transfer both need.
194    #[inline]
195    pub fn run(&self, access: Access<'_>, _cs: CriticalSection<'_>, src: &OamShadow) {
196        // 640 dots is longer than any blanking period but a VBlank's, so mode 0
197        // is no help here and the line is what decides. Reading 151 leaves 913
198        // dots, and 152 only 457; with the LCD off nothing is drawn and nothing
199        // has to be waited for.
200        if matches!(access, Access::Polled) && LCDC.read().lcd_enable() {
201            while !(144..=151).contains(&crate::mmio::LY.read()) {}
202        }
203        (self.0)((src as *const OamShadow as usize >> 8) as u8);
204    }
205}