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