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}