Skip to main content

gb/
ppu.rs

1//! The pixel-processing unit, which draws the screen.
2//!
3//! The picture comes from six modules: [`tile`] holds the pixels, [`map`] names
4//! which tile goes in which cell, [`bg`] and [`window`] place the two layers
5//! that read a map, [`obj`] carries the sprites drawn over them, and
6//! [`palette`] decides what a pixel's colour index becomes.
7//!
8//! # Reaching video memory
9//!
10//! VRAM and OAM are not always the CPU's to write. The PPU takes them while it
11//! draws, and a write made then is dropped. Every function that writes takes an
12//! [`Access`], which records how the caller knows its write will land.
13//!
14//! [`Direct`](Access::Direct) means the caller is already inside a VBlank or has
15//! the LCD switched off. It is the cheap one, and [`Vblank::with`] and
16//! [`with_lcd_off`] are what produce it. The window's length is not enforced, so
17//! a closure that runs past the end of a VBlank loses the rest of its writes.
18//!
19//! [`Polled`](Access::Polled) claims nothing about when it is called, and waits
20//! for the PPU itself instead. It reaches far more of the frame than VBlank
21//! alone, since HBlank recurs on every line, and it blocks until the whole write
22//! is through. For reference, every video write in GBDK works this way.
23//!
24//! ```ignore
25//! let vblank = unsafe { Vblank::listen() };
26//!
27//! // A frame's worth of updates, taken inside the window
28//! vblank.with(|d| {
29//!     map::write(d, bg::map(), col % 32, 0, LEVEL.sub(col, 0, 1, 18));
30//!     obj::set(d, 0, player);
31//! });
32//!
33//! // A bulk load: the screen goes blank, and the window has no deadline
34//! ppu::with_lcd_off(|d| tile::write_all(d, 0, &TILESET));
35//!
36//! // Nowhere in particular: let the write wait for the PPU itself
37//! tile::write(Access::Polled, 5, &spark);
38//! ```
39//!
40//! Work that has to happen between frames belongs before [`Vblank::with`], not
41//! inside it: decide what to draw first, then take the window and spend it on
42//! writes.
43//!
44//! # Writing from a handler
45//!
46//! Much of what follows reads a register, changes a bit and writes it back,
47//! `LCDC` most of all: [`bg`], [`window`], [`obj`] and [`tile`] each own a bit
48//! of it and leave the rest alone. A handler doing the same to the same register
49//! races whatever it interrupted, and one of the two changes is lost.
50//!
51//! [`palette`] fares worse. Its colours travel through an index register, set
52//! once and stepped by the hardware, so a handler writing a palette of its own
53//! sends the rest of the interrupted write into whichever palette it left
54//! selected.
55//!
56//! So a program driving a raster effect from a `STAT` handler should keep its
57//! other writes to those registers in VBlank, where the two cannot overlap.
58//!
59//! # Frame pacing
60//!
61//! A program that uses this module links a weak `_on_vblank` advancing a frame
62//! counter. `#[gb::rt::interrupt(VBlank)]` replaces it, so that handler has to
63//! call [`frame_tick`] itself or [`Vblank::wait`] never returns.
64
65pub mod bg;
66#[cfg(feature = "cgb")]
67#[cfg_attr(docsrs, doc(cfg(feature = "cgb")))]
68pub mod hdma;
69pub mod map;
70pub mod obj;
71pub mod palette;
72pub mod tile;
73pub mod window;
74
75use core::marker::PhantomData;
76
77use gb_hram::HramAtomicAccess;
78
79// One byte, so each access is a single `ldh` that cannot tear against the
80// handler, and the wrap every 256 frames is harmless to the inequality
81// `Vblank::wait` compares with.
82crate::hram! {
83    static FRAME: HramAtomicCell<u8>;
84}
85
86// Weak, so a handler in the program replaces it. Not `pub`: the symbol is what
87// the vector needs, and calling this as a function would return through `reti`.
88#[linkage = "weak"]
89#[unsafe(no_mangle)]
90extern "z80-interrupt" fn on_vblank() {
91    frame_tick();
92}
93
94/// How a write reaches video memory.
95///
96/// VRAM at `0x8000` and OAM at `0xFE00` are locked on different schedules: VRAM
97/// is reachable except in mode 3, OAM except in modes 2 and 3, and turning the
98/// LCD off opens both. See
99/// <https://gbdev.io/pandocs/Accessing_VRAM_and_OAM.html>.
100///
101/// Where the hardware has them locked it ignores writes and reads back `0xFF`,
102/// so a value written there is lost rather than wrong, and nothing reports it.
103#[derive(Clone, Copy)]
104pub enum Access<'a> {
105    /// The caller is already inside a window where both are open, so writes go
106    /// straight through.
107    ///
108    /// Minted by [`Vblank::with`] and [`with_lcd_off`], and bounded to the closure
109    /// they run. It is zero-sized and `Copy`; the lifetime is the only thing
110    /// stopping it being carried out of the window:
111    ///
112    /// ```compile_fail
113    /// # use gb::ppu::{Access, Vblank};
114    /// # let vblank = unsafe { Vblank::listen() };
115    /// let mut saved: Option<Access> = None;
116    /// vblank.with(|d| { saved = Some(d); }); // ERROR: `d` escapes the closure
117    /// ```
118    #[non_exhaustive]
119    Direct(PhantomData<&'a ()>),
120
121    /// Wait for the PPU to release video memory before writing, which makes the
122    /// call safe at any point in the frame.
123    ///
124    /// This reaches far more of the frame than one VBlank does, since HBlank
125    /// recurs on every line. What it costs is the wait, which a bulk write pays
126    /// for every byte: one this way can hold the CPU for more than a frame while
127    /// the picture stays up.
128    Polled,
129}
130
131// A proof about the PPU's current state belongs to the context that read it.
132impl<'a> !Send for Access<'a> {}
133impl<'a> !Sync for Access<'a> {}
134
135impl<'a> Access<'a> {
136    /// Mint [`Direct`](Access::Direct), asserting that video memory is reachable.
137    ///
138    /// The escape hatch for a context [`Vblank::with`] cannot serve, such as a
139    /// VBlank handler, which is already inside the window it would wait for.
140    ///
141    /// # Safety
142    ///
143    /// The PPU must be in mode 0 or 1, or the LCD off. The returned lifetime is
144    /// unconstrained, so the caller must bound it to the period that holds.
145    pub const unsafe fn assume() -> Self {
146        Access::Direct(PhantomData)
147    }
148
149    /// Copy `src` into video memory at `dst`.
150    ///
151    /// # Safety
152    ///
153    /// `dst` must be a VRAM or OAM address with room for all of `src`.
154    pub(crate) unsafe fn write(self, dst: *mut u8, src: &[u8]) {
155        // One routine for VRAM and OAM: `wait_blank` covers the OAM lock as well
156        // as the VRAM one, so there is nothing left to tell the two apart.
157        //
158        // Resolve the discipline once. Monomorphising on it keeps the branch out
159        // of the loop, which matters where a run is only a byte or two long.
160        match self {
161            Access::Polled => unsafe { run::<true>(dst, src) },
162            _ => unsafe { run::<false>(dst, src) },
163        }
164    }
165}
166
167unsafe fn run<const WAIT: bool>(dst: *mut u8, src: &[u8]) {
168    // Walk the destination rather than indexing from `dst`: one fewer value live
169    // across the wait, in a loop the register allocator is already tight on.
170    let mut d = dst;
171    for b in src {
172        if WAIT {
173            wait_blank();
174        }
175        unsafe { core::ptr::write_volatile(d, *b) };
176        d = unsafe { d.add(1) };
177    }
178}
179
180/// Block until the PPU is between lines or between frames.
181///
182/// Modes 0 and 1 are the two, and seeing either leaves at least 80 dots before
183/// video memory is taken again. Waiting only for mode 3 to pass would not: mode
184/// 2 can be a dot from mode 3, and a write made on the strength of that check
185/// would be dropped after it.
186#[inline(always)]
187pub(crate) fn wait_blank() {
188    use crate::mmio::PpuMode;
189    while matches!(
190        crate::mmio::STAT.read().mode(),
191        PpuMode::OamScan | PpuMode::Drawing
192    ) {}
193}
194
195/// Advance the frame counter.
196///
197/// Needed only by a VBlank handler that replaced the one this module installs.
198#[inline(always)]
199pub fn frame_tick() {
200    // A read-modify-write, and single-writer: the VBlank handler is the only
201    // caller that matters, and the CPU clears IME on dispatch, so it cannot
202    // interrupt itself.
203    FRAME.set(FRAME.get().wrapping_add(1));
204}
205
206/// The frame clock: proof that the VBlank interrupt is reaching the counter.
207///
208/// Everything that waits for a frame hangs without it.
209pub struct Vblank(());
210
211// A proof about the hardware's current state belongs to the context that made it.
212impl !Send for Vblank {}
213impl !Sync for Vblank {}
214
215impl Vblank {
216    /// Listen for the VBlank interrupt.
217    ///
218    /// # Safety
219    ///
220    /// Turns interrupts on. That is preemption, which the surrounding code may
221    /// have been written to rule out.
222    #[inline]
223    pub unsafe fn listen() -> Self {
224        // `IE` is read and written back, and another module may have turned
225        // interrupts on already, so the pair is kept off the air.
226        crate::interrupt::disable();
227        unsafe {
228            crate::interrupt::set_enabled(
229                crate::interrupt::enabled() | crate::mmio::Interrupts::VBLANK,
230            );
231            crate::interrupt::enable();
232        }
233        Vblank(())
234    }
235
236    /// Frames counted since boot, wrapping at 256.
237    ///
238    /// [`wrapping_sub`](u8::wrapping_sub) of two readings is the frames between
239    /// them; a plain subtraction is what overflows across the wrap.
240    #[inline(always)]
241    pub fn frame_count(&self) -> u8 {
242        FRAME.get()
243    }
244
245    /// Block until the next frame.
246    ///
247    /// The CPU sleeps while waiting. Does not return with the LCD off, since no
248    /// VBlank arrives then, nor where a replacement handler skips
249    /// [`frame_tick`].
250    pub fn wait(&self) {
251        let seen = FRAME.get();
252        loop {
253            // VBlank is requested at dot 0 of line 144, so LY reading 143 can be
254            // a single dot away from it, and halting there would sleep through
255            // the frame being waited for. Every other line leaves at least 457
256            // dots, against the ~60 this takes to reach the halt.
257            //
258            // LY is read before the counter so a VBlank landing between the two
259            // is caught by the counter read rather than missed. Volatile and
260            // `asm!` accesses keep that order.
261            let near_vblank = crate::mmio::LY.read() == 143;
262            if FRAME.get() != seen {
263                return;
264            }
265            if !near_vblank {
266                crate::interrupt::halt();
267            }
268        }
269    }
270
271    /// Wait for the next frame, then run `f` inside its VBlank.
272    ///
273    /// Nothing enforces the window. `f` keeps running once the PPU has moved on,
274    /// and writes past that point are dropped in silence.
275    ///
276    /// ```ignore
277    /// vblank.with(|d| {
278    ///     bg::set_scroll(d, camera_x, camera_y);
279    ///     obj::set(d, 0, player);
280    /// });
281    ///
282    /// // Overruns: the tail of the tileset lands after the window and is lost.
283    /// // `with_lcd_off` has no deadline.
284    /// vblank.with(|d| load_tileset(&TILES, d));
285    /// ```
286    ///
287    /// Waiting goes through [`wait`](Self::wait), so this does not return under
288    /// the same conditions.
289    pub fn with<R>(&self, f: impl FnOnce(Access<'_>) -> R) -> R {
290        self.wait();
291        f(unsafe { Access::assume() })
292    }
293}
294
295/// Turn the LCD off for the length of `f`.
296///
297/// The screen goes blank, and in exchange video memory stays reachable for as
298/// long as `f` runs rather than the roughly 1140 M-cycles [`Vblank::with`]
299/// allows, or 2280 in CGB double speed mode. A program with more tiles than one
300/// VBlank fits loads them this way.
301///
302/// Unlike [`Vblank::with`], the wait here is a poll and needs no interrupt, so
303/// this runs before a program has turned them on.
304///
305/// [`Vblank::wait`] does not return inside `f`, since no VBlank arrives with the LCD
306/// off. An LCD that was already off is left that way and `f` runs at once.
307///
308/// Turning it back on restarts the PPU at line 0, and the screen stays blank
309/// through that first frame. See <https://gbdev.io/pandocs/LCDC.html>.
310pub fn with_lcd_off<R>(f: impl FnOnce(Access<'_>) -> R) -> R {
311    let lcdc = crate::mmio::LCDC.read();
312
313    // Waiting with the LCD already off would never return, and a caller that
314    // switched it off means to keep it off.
315    if !lcdc.lcd_enable() {
316        return f(unsafe { Access::assume() });
317    }
318
319    // Clearing the enable bit outside VBlank can damage the panel, so wait for
320    // one. The line is polled rather than waited on through `Vblank`: this runs
321    // before interrupts are on, which is where a program loads its tiles,
322    // and it leaves `IME` alone rather than ending a critical section it was
323    // called inside.
324    //
325    // The first loop skips a VBlank already under way, so the second catches the
326    // next one near its start. A handler landing between that read and the write
327    // would have to outlast the rest of the VBlank to push the write out of it.
328    while crate::mmio::LY.read() >= 146 {}
329    while crate::mmio::LY.read() < 145 {}
330
331    // Re-read: a handler may have reconfigured the PPU during the wait.
332    unsafe { crate::mmio::LCDC.write(crate::mmio::LCDC.read().with_lcd_enable(false)) };
333
334    let r = f(unsafe { Access::assume() });
335
336    // Re-read rather than writing `lcdc` back: `f` may have reconfigured the
337    // PPU, and only the enable bit is this function's to restore.
338    unsafe { crate::mmio::LCDC.write(crate::mmio::LCDC.read().with_lcd_enable(true)) };
339    r
340}
341
342/// Which half of the Game Boy Color's video memory the CPU sees at `0x8000`.
343///
344/// The PPU reads both halves through its own paths, so this is only about CPU
345/// access.
346#[cfg(feature = "cgb")]
347#[cfg_attr(docsrs, doc(cfg(feature = "cgb")))]
348#[derive(Clone, Copy, PartialEq, Eq, Debug)]
349pub enum VramBank {
350    /// Tile slots and the two tilemaps, as on the original Game Boy.
351    Zero = 0,
352    /// A second set of tile slots, and per-cell attributes where bank zero holds
353    /// the tilemaps.
354    One = 1,
355}
356
357/// Run `f` with `bank` mapped at `0x8000`, then put back the one that was there.
358///
359/// The Game Boy Color banks all of `0x8000..0xA000`, so the same
360/// [`tile`] or map write lands in a different place depending on this. Bracketing
361/// it keeps that visible at the call site and out of the surrounding code.
362///
363/// `f` takes nothing: the [`Access`] an enclosing [`Vblank::with`] or
364/// [`with_lcd_off`] handed out is still good here, since which bank is mapped and
365/// whether video memory is reachable are separate questions. The two scopes nest
366/// in either order.
367///
368/// A handler that changes the bank must put it back, the way one that switches
369/// ROM banks must, and one that writes video memory has to set the bank it means
370/// rather than assume zero: it may well have interrupted this.
371///
372/// An original Game Boy has no such register: the write goes nowhere and `f` runs against
373/// the one bank there is, overwriting what was already in it. A cartridge that
374/// runs on both machines has to ask [`is_cgb`](crate::is_cgb) first, there being
375/// no second bank to fall back to.
376///
377/// ```ignore
378/// ppu::with_lcd_off(|d| {
379///     tile::write_all(d, 0, &TILES);
380///     ppu::with_vram_bank(VramBank::One, || tile::write_all(d, 0, &MORE));
381/// });
382/// ```
383#[cfg(feature = "cgb")]
384#[cfg_attr(docsrs, doc(cfg(feature = "cgb")))]
385#[inline]
386pub fn with_vram_bank<R>(bank: VramBank, f: impl FnOnce() -> R) -> R {
387    // Bit 0 is the bank; the rest read as ones.
388    let saved = crate::mmio::cgb::VBK.read() & 1;
389    crate::mmio::cgb::VBK.write(bank as u8);
390    let r = f();
391    crate::mmio::cgb::VBK.write(saved);
392    r
393}