gb/ppu/window.rs
1//! The window: a second tilemap laid over the background.
2//!
3//! It has no scroll of its own. Wherever it is placed, it draws from its grid's
4//! top left corner, so moving what it shows means rewriting the grid. Staying
5//! put is what suits it to a status bar or a text box while the background
6//! scrolls underneath.
7//!
8//! # Position
9//!
10//! The hardware register is the screen column *plus seven*, so `WX` of 7 is the
11//! left edge. [`set_position`] takes the screen column and adds it, which also
12//! puts the off-screen values out of reach: `WX` below 7 starts the window left
13//! of the screen and is where the hardware behaves least predictably. Reach for
14//! [`mmio::WX`](crate::mmio::WX) directly if that is wanted.
15//!
16//! # Writing mid-frame
17//!
18//! `WX`, `WY` and the enable bit are least glitchy written during VBlank, or
19//! during HBlank where they must change mid-frame, which is what the [`Access`]
20//! here is for. [`set_map`] is not among them: it takes effect from the next
21//! tile fetched and has nothing to go wrong.
22//!
23//! Use [`hide`] where the window has to come and go within a frame, not the
24//! enable bit. The PPU turns the window on for a frame when `WY`
25//! first matches `LY`, and on the Game Boy Color clearing the enable bit undoes
26//! that: the window then stays away until `WY` matches again, which for the rest
27//! of the frame it cannot. See <https://gbdev.io/pandocs/Window.html>.
28
29use crate::mmio::{LCDC, WX, WY};
30
31use super::map::Map;
32use super::{Access, wait_blank};
33
34/// What `WX` is offset by: `WX` of 7 is screen column 0.
35pub const X_OFFSET: u8 = 7;
36
37/// Where the window's top left corner sits on screen, as `(x, y)`.
38#[inline]
39pub fn position() -> (u8, u8) {
40 (WX.read().saturating_sub(X_OFFSET), WY.read())
41}
42
43/// Put the window's top left corner at `(x, y)` on screen.
44///
45/// An `x` of 160 or more, or a `y` of 144 or more, leaves it off screen; [`hide`]
46/// is the way to say that on purpose.
47#[inline]
48pub fn set_position(access: Access<'_>, x: u8, y: u8) {
49 if matches!(access, Access::Polled) {
50 wait_blank();
51 }
52 WX.write(x.saturating_add(X_OFFSET));
53 WY.write(y);
54}
55
56/// Take the window off screen without disturbing the enable bit.
57#[inline]
58pub fn hide(access: Access<'_>) {
59 if matches!(access, Access::Polled) {
60 wait_blank();
61 }
62 // One past the last visible `WX`, and so also past the monochrome bug at 166
63 // where the window spans the screen shifted down a line.
64 WX.write(167);
65}
66
67/// Whether the PPU draws the window at all, from `LCDC` bit 5.
68#[inline]
69pub fn enabled() -> bool {
70 LCDC.read().window_enable()
71}
72
73/// Set `LCDC` bit 5.
74///
75/// For taking the window away and bringing it back within a frame, use [`hide`]
76/// and [`set_position`]: on the Game Boy Color, clearing this bit also clears the
77/// condition that lets the window appear at all, and it will not come back that
78/// frame.
79#[inline]
80pub fn set_enabled(access: Access<'_>, on: bool) {
81 if matches!(access, Access::Polled) {
82 wait_blank();
83 }
84 // Read-modify-write: the LCD enable bit is the only reason this is unsafe.
85 unsafe { LCDC.write(LCDC.read().with_window_enable(on)) };
86}
87
88/// Which grid the window reads, from `LCDC` bit 6.
89#[inline]
90pub fn map() -> Map {
91 if LCDC.read().window_tilemap_high() { Map::One } else { Map::Zero }
92}
93
94/// Point the window at a grid.
95#[inline]
96pub fn set_map(map: Map) {
97 unsafe { LCDC.write(LCDC.read().with_window_tilemap_high(map == Map::One)) };
98}