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