Skip to main content

dotzuki_renderer/
window_layer.rs

1//! Window layer — the Game Boy's WINDOW overlay.
2//!
3//! The Game Boy WINDOW is an overlay that can cover part of the screen.
4//! It has its own tile map and is positioned via WX (window X + 7) and WY.
5//! When enabled, it replaces the BG for all pixels at and below WY,
6//! starting from screen column (WX - 7).
7//!
8//! Unlike BG, the window does NOT wrap — it renders from top-left of its
9//! own tile map, with no scroll offset.
10
11use crate::palette::Palette;
12use crate::tile::TileSet;
13use crate::tilemap::BG_MAP_PIXEL_HEIGHT;
14use crate::tilemap::BG_MAP_PIXEL_WIDTH;
15use dotzuki_engine::tilemap::Tilemap;
16use crate::{FbSurface, TILE_SIZE};
17
18/// Window layer state.
19#[derive(Debug, Clone)]
20pub struct WindowLayer {
21    /// The window's tile map (same format as BG: 32×32 entries).
22    pub tilemap: Tilemap,
23    /// Window X position register (WX). The window starts at screen column (wx - 7).
24    /// Valid range: 0–166. A value of 7 means the window starts at screen column 0.
25    pub wx: u32,
26    /// Window Y position register (WY). The window starts at this screen row.
27    /// Valid range: 0–143.
28    pub wy: u32,
29    /// Whether the window layer is enabled.
30    pub enabled: bool,
31}
32
33impl WindowLayer {
34    pub fn new() -> Self {
35        Self {
36            tilemap: Tilemap::new(32, 32),
37            wx: 7, // Default: window at left edge
38            wy: 0, // Default: window at top
39            enabled: false,
40        }
41    }
42
43    /// The actual screen X where the window starts.
44    /// WX=7 means column 0. WX<7 is clamped to 0.
45    #[inline]
46    pub fn screen_x(&self) -> u32 {
47        if self.wx < 7 {
48            0
49        } else {
50            self.wx - 7
51        }
52    }
53
54    /// Render the window layer on top of the existing framebuffer content.
55    /// Only renders pixels where the window is visible (at and below WY,
56    /// at and right of WX-7).
57    pub fn render(&self, fb: &mut impl FbSurface, tileset: &TileSet, palette: &Palette) {
58        if !self.enabled {
59            return;
60        }
61
62        let win_start_x = self.screen_x();
63        if win_start_x >= fb.width() || self.wy >= fb.height() {
64            return;
65        }
66
67        for screen_y in self.wy..fb.height() {
68            self.render_scanline(fb, tileset, palette, screen_y);
69        }
70    }
71
72    /// Render a single scanline of the window layer.
73    pub fn render_scanline(
74        &self,
75        fb: &mut impl FbSurface,
76        tileset: &TileSet,
77        palette: &Palette,
78        screen_y: u32,
79    ) {
80        if !self.enabled || screen_y < self.wy {
81            return;
82        }
83
84        let win_start_x = self.screen_x();
85        if win_start_x >= fb.width() {
86            return;
87        }
88
89        // Window-space coordinates: the window renders from its own (0,0)
90        let win_y = screen_y - self.wy;
91        if win_y >= BG_MAP_PIXEL_HEIGHT {
92            return;
93        }
94
95        let tile_row = win_y / TILE_SIZE;
96        let pixel_row = (win_y % TILE_SIZE) as usize;
97
98        for screen_x in win_start_x..fb.width() {
99            let win_x = screen_x - win_start_x;
100            if win_x >= BG_MAP_PIXEL_WIDTH {
101                break;
102            }
103
104            let tile_col = win_x / TILE_SIZE;
105            let pixel_col = (win_x % TILE_SIZE) as usize;
106
107            let tile_index = self
108                .tilemap
109                .get(tile_col as u16, tile_row as u16)
110                .map(|e| e.tile_id as usize)
111                .unwrap_or(0);
112            let tile = tileset.get(tile_index);
113            let color_idx = tile.get(pixel_row, pixel_col);
114
115            let rgba = palette.color(crate::palette::GbColor::from_u8(color_idx));
116            fb.set_pixel(screen_x, screen_y, rgba);
117        }
118    }
119}
120
121impl Default for WindowLayer {
122    fn default() -> Self {
123        Self::new()
124    }
125}