dotzuki_renderer/
sprite.rs1use crate::palette::{ColorIndex, GbaColor, GbColor, Palette};
17use crate::tile::{TileFormat, TileSet, TILE_PIXELS};
18use crate::FbSurface;
19
20pub const OAM_PRIORITY: u8 = 0x80; pub const OAM_Y_FLIP: u8 = 0x40; pub const OAM_X_FLIP: u8 = 0x20; pub const OAM_PALETTE: u8 = 0x10; pub const MAX_OAM_ENTRIES: usize = 40;
28
29#[derive(Debug, Clone, Copy, Default)]
31pub struct SpriteOamEntry {
32 pub y: i32,
34 pub x: i32,
36 pub tile_id: u8,
38 pub attributes: u8,
40}
41
42impl SpriteOamEntry {
43 pub fn new(y: i32, x: i32, tile_id: u8, attributes: u8) -> Self {
44 Self {
45 y,
46 x,
47 tile_id,
48 attributes,
49 }
50 }
51
52 pub fn from_raw(raw_y: u8, raw_x: u8, tile_id: u8, attributes: u8) -> Self {
54 Self {
55 y: raw_y as i32 - 16,
56 x: raw_x as i32 - 8,
57 tile_id,
58 attributes,
59 }
60 }
61
62 #[inline]
63 pub fn bg_priority(&self) -> bool {
64 self.attributes & OAM_PRIORITY != 0
65 }
66
67 #[inline]
68 pub fn y_flip(&self) -> bool {
69 self.attributes & OAM_Y_FLIP != 0
70 }
71
72 #[inline]
73 pub fn x_flip(&self) -> bool {
74 self.attributes & OAM_X_FLIP != 0
75 }
76
77 #[inline]
78 pub fn uses_obp1(&self) -> bool {
79 self.attributes & OAM_PALETTE != 0
80 }
81
82 pub fn is_on_screen(&self, screen_width: u32, screen_height: u32) -> bool {
84 let x_end = self.x + TILE_PIXELS as i32;
85 let y_end = self.y + TILE_PIXELS as i32;
86 x_end > 0 && self.x < screen_width as i32 && y_end > 0 && self.y < screen_height as i32
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct SpriteLayer {
93 pub entries: Vec<SpriteOamEntry>,
94}
95
96impl SpriteLayer {
97 pub fn new() -> Self {
98 Self {
99 entries: Vec::with_capacity(MAX_OAM_ENTRIES),
100 }
101 }
102
103 pub fn clear(&mut self) {
104 self.entries.clear();
105 }
106
107 pub fn add(&mut self, entry: SpriteOamEntry) {
108 self.entries.push(entry);
109 }
110
111 pub fn render(
119 &self,
120 fb: &mut impl FbSurface,
121 tileset: &TileSet,
122 obp0: &Palette,
123 obp1: &Palette,
124 bg_color_buffer: Option<&[u8]>,
125 ) {
126 for entry in self.entries.iter().rev() {
128 render_sprite(fb, tileset, obp0, obp1, entry, bg_color_buffer);
129 }
130 }
131}
132
133impl Default for SpriteLayer {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139fn render_sprite(
146 fb: &mut impl FbSurface,
147 tileset: &TileSet,
148 obp0: &Palette,
149 obp1: &Palette,
150 entry: &SpriteOamEntry,
151 bg_color_buffer: Option<&[u8]>,
152) {
153 if !entry.is_on_screen(fb.width(), fb.height()) {
154 return;
155 }
156
157 let format = tileset.tile_format();
158 let fb_w = fb.width() as usize;
159 let fb_h = fb.height() as i32;
160
161 if format == TileFormat::FullColor {
163 if let Some(rgba_tile) = tileset.get_rgba(entry.tile_id as usize) {
164 for row in 0..TILE_PIXELS {
165 let screen_y = entry.y + row as i32;
166 if screen_y < 0 || screen_y >= fb_h {
167 continue;
168 }
169 let tile_row = if entry.y_flip() {
170 TILE_PIXELS - 1 - row
171 } else {
172 row
173 };
174 for col in 0..TILE_PIXELS {
175 let screen_x = entry.x + col as i32;
176 if screen_x < 0 || screen_x >= fb.width() as i32 {
177 continue;
178 }
179 let tile_col = if entry.x_flip() {
180 TILE_PIXELS - 1 - col
181 } else {
182 col
183 };
184 let rgba = rgba_tile.pixels[tile_row][tile_col];
185 if rgba.a == 0 {
187 continue;
188 }
189 if entry.bg_priority() {
191 if let Some(bg_buf) = bg_color_buffer {
192 let pixel_offset = screen_y as usize * fb_w + screen_x as usize;
193 if pixel_offset < bg_buf.len() && bg_buf[pixel_offset] != 0 {
194 continue;
195 }
196 }
197 }
198 fb.set_pixel(screen_x as u32, screen_y as u32, rgba);
199 }
200 }
201 }
202 return;
203 }
204
205 let tile = tileset.get(entry.tile_id as usize);
207 let palette = if entry.uses_obp1() { obp1 } else { obp0 };
208 let is_gba = format == TileFormat::Gba4bpp;
209
210 for row in 0..TILE_PIXELS {
211 let screen_y = entry.y + row as i32;
212 if screen_y < 0 || screen_y >= fb_h {
213 continue;
214 }
215
216 let tile_row = if entry.y_flip() {
217 TILE_PIXELS - 1 - row
218 } else {
219 row
220 };
221
222 for col in 0..TILE_PIXELS {
223 let screen_x = entry.x + col as i32;
224 if screen_x < 0 || screen_x >= fb.width() as i32 {
225 continue;
226 }
227
228 let tile_col = if entry.x_flip() {
229 TILE_PIXELS - 1 - col
230 } else {
231 col
232 };
233
234 let color_idx = tile.get(tile_row, tile_col);
235
236 if color_idx == 0 {
238 continue;
239 }
240
241 if entry.bg_priority() {
243 if let Some(bg_buf) = bg_color_buffer {
244 let pixel_offset =
245 screen_y as usize * fb_w + screen_x as usize;
246 if pixel_offset < bg_buf.len() && bg_buf[pixel_offset] != 0 {
247 continue;
248 }
249 }
250 }
251
252 let rgba = if is_gba {
253 palette.colors[GbaColor::from_u8(color_idx).to_index()]
254 } else {
255 palette.color(GbColor::from_u8(color_idx))
256 };
257 fb.set_pixel(screen_x as u32, screen_y as u32, rgba);
258 }
259 }
260}