yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
room_layer_manager.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <array>
5#include <climits>
6#include <vector>
7
8#include "SDL.h"
10#include "util/log.h"
11
12namespace yaze {
13namespace zelda3 {
14
15namespace {
16
17// Helper to copy SDL palette from source surface to destination bitmap
18// Uses vector extraction + SetPalette for reliable palette application
19void ApplySDLPaletteToBitmap(SDL_Surface* src_surface, gfx::Bitmap& dst_bitmap) {
20 if (!src_surface || !src_surface->format) return;
21
22 SDL_Palette* src_pal = src_surface->format->palette;
23 if (!src_pal || src_pal->ncolors == 0) return;
24
25 // Extract palette colors into a vector
26 std::vector<SDL_Color> colors(256);
27 int colors_to_copy = std::min(src_pal->ncolors, 256);
28 for (int i = 0; i < colors_to_copy; ++i) {
29 colors[i] = src_pal->colors[i];
30 }
31
32 // Fill remaining with transparent black (prevents undefined colors)
33 for (int i = colors_to_copy; i < 256; ++i) {
34 colors[i] = {0, 0, 0, 0};
35 }
36
37 // Dungeon rendering uses palette index 255 as the "undrawn/transparent" fill.
38 // Palette index 0 is not written by our tile renderer (pixel value 0 is skipped),
39 // so we reserve it as an opaque backdrop color for the composited output.
40 colors[0] = {0, 0, 0, 255};
41 colors[255] = {0, 0, 0, 0};
42
43 // Apply palette to destination bitmap using the reliable method
44 dst_bitmap.SetPalette(colors);
45}
46
47} // namespace
48
50 gfx::Bitmap& output) const {
51 constexpr int kWidth = 512;
52 constexpr int kHeight = 512;
53 constexpr int kPixelCount = kWidth * kHeight;
54
55 // Log layer visibility and blend modes (once per room)
56 static int last_room_id = -1;
57 if (room.id() != last_room_id) {
58 last_room_id = room.id();
59 LOG_DEBUG("LayerManager", "Room %03X: BG1_Layout(vis=%d,blend=%d) "
60 "BG1_Objects(vis=%d,blend=%d) BG2_Layout(vis=%d,blend=%d) "
61 "BG2_Objects(vis=%d,blend=%d) MergeType=%d",
62 room.id(),
72 }
73
74 // Ensure output bitmap is properly sized
75 if (output.width() != kWidth || output.height() != kHeight) {
76 output.Create(kWidth, kHeight, 8,
77 std::vector<uint8_t>(kPixelCount, 0));
78 } else {
79 // Clear to backdrop (0). Transparent pixels (255) from layers will reveal
80 // this backdrop, matching SNES behavior when all layers are transparent.
81 output.Fill(0);
82 }
83
84 // Track if we've copied the palette yet
85 bool palette_copied = false;
86
87 // Get all 4 layer buffers
88 auto& bg1_layout = GetLayerBuffer(room, LayerType::BG1_Layout);
89 auto& bg1_objects = GetLayerBuffer(room, LayerType::BG1_Objects);
90 auto& bg2_layout = GetLayerBuffer(room, LayerType::BG2_Layout);
91 auto& bg2_objects = GetLayerBuffer(room, LayerType::BG2_Objects);
92
93 // Copy palette from first available visible layer
94 auto CopyPaletteIfNeeded = [&](const gfx::Bitmap& src_bitmap) {
95 if (!palette_copied && src_bitmap.surface()) {
96 ApplySDLPaletteToBitmap(src_bitmap.surface(), output);
97 palette_copied = true;
98 }
99 };
100
102 // Priority compositing (SNES Mode 1):
103 // - BG2 priority=0 is behind BG1 priority=0.
104 // - BG2 priority=1 can appear above BG1 priority=0.
105 // - BG1 priority=1 is above BG2 priority=1.
106 //
107 // We first combine Layout+Objects for each BG (objects overwrite layout),
108 // then resolve BG1 vs BG2 per-pixel using the stored priority buffers.
109 // When BG2 is translucent (water rooms, color math), we blend colors
110 // using the SDL palette instead of simple overwrite.
111 auto layer_enabled = [&](LayerType type, const gfx::BackgroundBuffer& buf) {
112 if (!IsLayerVisible(type)) {
113 return false;
114 }
116 return false;
117 }
118 const auto& bmp = buf.bitmap();
119 if (!bmp.is_active() || bmp.width() == 0) {
120 return false;
121 }
122 return true;
123 };
124
125 // Ensure the output palette matches the room's SDL palette.
126 if (layer_enabled(LayerType::BG1_Layout, bg1_layout)) {
127 CopyPaletteIfNeeded(bg1_layout.bitmap());
128 }
129 if (layer_enabled(LayerType::BG1_Objects, bg1_objects)) {
130 CopyPaletteIfNeeded(bg1_objects.bitmap());
131 }
132 if (layer_enabled(LayerType::BG2_Layout, bg2_layout)) {
133 CopyPaletteIfNeeded(bg2_layout.bitmap());
134 }
135 if (layer_enabled(LayerType::BG2_Objects, bg2_objects)) {
136 CopyPaletteIfNeeded(bg2_objects.bitmap());
137 }
138
139 // Check if BG2 uses translucent blending (water rooms, color math effects).
140 // When translucent, overlapping BG1+BG2 pixels are averaged in RGB space.
141 const bool bg2_translucent =
144
145 // Build palette lookup table for color blending (only when needed).
146 // Extract from the output bitmap's SDL palette so we can do RGB math.
147 std::vector<SDL_Color> pal_lut;
148 if (bg2_translucent && output.surface() && output.surface()->format &&
149 output.surface()->format->palette) {
150 SDL_Palette* sdl_pal = output.surface()->format->palette;
151 int n = std::min(sdl_pal->ncolors, 256);
152 pal_lut.resize(256, {0, 0, 0, 0});
153 for (int i = 0; i < n; ++i) {
154 pal_lut[i] = sdl_pal->colors[i];
155 }
156 }
157
158 // Nearest-color lookup for blended result.
159 // Searches within the same 16-color bank as the BG1 pixel to preserve
160 // palette coherence (avoids cross-bank color artifacts).
161 auto find_nearest_in_bank = [&](uint8_t base_idx, uint8_t r, uint8_t g,
162 uint8_t b) -> uint8_t {
163 if (pal_lut.empty()) return base_idx;
164 int bank_start = (base_idx / 16) * 16;
165 int bank_end = bank_start + 16;
166 int best_idx = base_idx;
167 int best_dist = INT_MAX;
168 for (int i = bank_start + 1; i < bank_end && i < 256; ++i) {
169 int dr = static_cast<int>(pal_lut[i].r) - r;
170 int dg = static_cast<int>(pal_lut[i].g) - g;
171 int db = static_cast<int>(pal_lut[i].b) - b;
172 int dist = dr * dr + dg * dg + db * db;
173 if (dist < best_dist) {
174 best_dist = dist;
175 best_idx = i;
176 }
177 }
178 return static_cast<uint8_t>(best_idx);
179 };
180
181 // Cache resolved blended indices to avoid repeating nearest-bank searches
182 // for the same winner/other palette pair in this frame.
183 std::array<uint8_t, 256 * 256> blend_cache{};
184 std::array<uint8_t, 256 * 256> blend_cache_valid{};
185 auto resolve_blended_index = [&](uint8_t winner_idx,
186 uint8_t other_idx) -> uint8_t {
187 if (pal_lut.empty() || winner_idx == other_idx) {
188 return winner_idx;
189 }
190 const size_t key =
191 (static_cast<size_t>(winner_idx) << 8) | static_cast<size_t>(other_idx);
192 if (blend_cache_valid[key] != 0) {
193 return blend_cache[key];
194 }
195
196 const SDL_Color& c1 = pal_lut[winner_idx];
197 const SDL_Color& c2 = pal_lut[other_idx];
198 const uint8_t blend_r = static_cast<uint8_t>((c1.r + c2.r) / 2);
199 const uint8_t blend_g = static_cast<uint8_t>((c1.g + c2.g) / 2);
200 const uint8_t blend_b = static_cast<uint8_t>((c1.b + c2.b) / 2);
201 const uint8_t resolved =
202 find_nearest_in_bank(winner_idx, blend_r, blend_g, blend_b);
203 blend_cache[key] = resolved;
204 blend_cache_valid[key] = 1;
205 return resolved;
206 };
207
208 auto normalize_pri = [](uint8_t pri) -> uint8_t {
209 // 0xFF is our "unset" value. Treat unset as low priority.
210 return (pri == 0xFF) ? 0 : (pri ? 1 : 0);
211 };
212
213 auto rank_for = [&](bool is_bg1, uint8_t pri) -> int {
214 pri = normalize_pri(pri);
215 if (is_bg1) {
216 return pri ? 3 : 1;
217 }
218 return pri ? 2 : 0;
219 };
220
221 const auto& bg1_layout_px = bg1_layout.bitmap().data();
222 const auto& bg1_obj_px = bg1_objects.bitmap().data();
223 const auto& bg2_layout_px = bg2_layout.bitmap().data();
224 const auto& bg2_obj_px = bg2_objects.bitmap().data();
225 const auto& bg1_layout_pri = bg1_layout.priority_data();
226 const auto& bg1_obj_pri = bg1_objects.priority_data();
227 const auto& bg2_layout_pri = bg2_layout.priority_data();
228 const auto& bg2_obj_pri = bg2_objects.priority_data();
229 const auto& bg1_obj_cov = bg1_objects.coverage_data();
230 const auto& bg2_obj_cov = bg2_objects.coverage_data();
231
232 const bool bg1_layout_on = layer_enabled(LayerType::BG1_Layout, bg1_layout);
233 const bool bg1_obj_on = layer_enabled(LayerType::BG1_Objects, bg1_objects);
234 const bool bg2_layout_on = layer_enabled(LayerType::BG2_Layout, bg2_layout);
235 const bool bg2_obj_on = layer_enabled(LayerType::BG2_Objects, bg2_objects);
236
237 auto& dst_data = output.mutable_data();
238 for (int idx = 0; idx < kPixelCount; ++idx) {
239 uint8_t bg1_pixel = 255;
240 uint8_t bg1_pri = 0;
241 const bool bg1_obj_wrote =
242 bg1_obj_on &&
243 ((idx < static_cast<int>(bg1_obj_cov.size()) && bg1_obj_cov[idx] != 0) ||
244 !IsTransparent(bg1_obj_px[idx]));
245 if (bg1_obj_wrote) {
246 bg1_pixel = bg1_obj_px[idx];
247 bg1_pri = bg1_obj_pri[idx];
248 } else if (bg1_layout_on && !IsTransparent(bg1_layout_px[idx])) {
249 bg1_pixel = bg1_layout_px[idx];
250 bg1_pri = bg1_layout_pri[idx];
251 }
252
253 uint8_t bg2_pixel = 255;
254 uint8_t bg2_pri = 0;
255 const bool bg2_obj_wrote =
256 bg2_obj_on &&
257 ((idx < static_cast<int>(bg2_obj_cov.size()) && bg2_obj_cov[idx] != 0) ||
258 !IsTransparent(bg2_obj_px[idx]));
259 if (bg2_obj_wrote) {
260 bg2_pixel = bg2_obj_px[idx];
261 bg2_pri = bg2_obj_pri[idx];
262 } else if (bg2_layout_on && !IsTransparent(bg2_layout_px[idx])) {
263 bg2_pixel = bg2_layout_px[idx];
264 bg2_pri = bg2_layout_pri[idx];
265 }
266
267 if (IsTransparent(bg1_pixel)) {
268 if (!IsTransparent(bg2_pixel)) {
269 dst_data[idx] = bg2_pixel;
270 }
271 continue;
272 }
273 if (IsTransparent(bg2_pixel)) {
274 dst_data[idx] = bg1_pixel;
275 continue;
276 }
277
278 // Both layers have opaque pixels. Resolve using priority + blend mode.
279 const int r1 = rank_for(/*is_bg1=*/true, bg1_pri);
280 const int r2 = rank_for(/*is_bg1=*/false, bg2_pri);
281
282 // SNES color math: when BG2 is translucent and both layers are visible,
283 // blend the two pixel colors using (main + sub) / 2 formula.
284 // This matches the SNES half-color-math used for water rooms.
285 if (bg2_translucent && !pal_lut.empty()) {
286 const bool bg1_wins = (r1 >= r2);
287 const uint8_t winner = bg1_wins ? bg1_pixel : bg2_pixel;
288 const uint8_t other = bg1_wins ? bg2_pixel : bg1_pixel;
289 dst_data[idx] = resolve_blended_index(winner, other);
290 } else {
291 dst_data[idx] = (r1 >= r2) ? bg1_pixel : bg2_pixel;
292 }
293 }
294 } else {
295 // Fallback to simple back-to-front layer order: BG2 then BG1.
296 //
297 // Blend Modes (from LayerMergeType):
298 // - Normal: Opaque pixels overwrite destination (standard)
299 // - Translucent: 50% alpha blend with destination
300 // - Addition: Additive color blending (SNES color math)
301 // - Dark: Darkened blend (reduced brightness)
302 // - Off: Layer is hidden
303 //
304 // IMPORTANT: Transparent pixels (255) in BG1 layers ALWAYS reveal BG2 beneath.
305 // This is how pits work: mask objects write 255 to BG1, allowing BG2 to show.
306 auto CompositeLayer = [&](gfx::BackgroundBuffer& buffer, LayerType layer_type) {
307 if (!IsLayerVisible(layer_type)) return;
308
309 const auto& src_bitmap = buffer.bitmap();
310 if (!src_bitmap.is_active() || src_bitmap.width() == 0) return;
311
312 LayerBlendMode blend_mode = GetLayerBlendMode(layer_type);
313 if (blend_mode == LayerBlendMode::Off) return;
314
315 CopyPaletteIfNeeded(src_bitmap);
316
317 const auto& src_data = src_bitmap.data();
318 auto& dst_data = output.mutable_data();
319
320 // Get layer alpha for translucent/dark blending
321 uint8_t layer_alpha = GetLayerAlpha(layer_type);
322
323 for (int idx = 0; idx < kPixelCount; ++idx) {
324 uint8_t src_pixel = src_data[idx];
325
326 // Skip transparent pixels (255 = fill color for undrawn areas)
327 // This is CRITICAL for pits: transparent BG1 pixels reveal BG2 beneath
328 if (IsTransparent(src_pixel)) continue;
329
330 // Apply blend mode
331 switch (blend_mode) {
333 // Standard opaque overwrite
334 dst_data[idx] = src_pixel;
335 break;
336
338 // 50% alpha blend: only overwrite if destination is transparent,
339 // otherwise blend colors using palette index averaging (simplified)
340 // For indexed color mode, we can't truly blend - use alpha threshold
341 if (IsTransparent(dst_data[idx]) || layer_alpha > 180) {
342 dst_data[idx] = src_pixel;
343 }
344 // If layer_alpha <= 180, destination shows through (simplified blend)
345 break;
346
348 // Additive blending: in indexed mode, just use source if visible
349 // True additive would need RGB values from palette
350 if (IsTransparent(dst_data[idx])) {
351 dst_data[idx] = src_pixel;
352 } else {
353 dst_data[idx] = src_pixel;
354 }
355 break;
356
358 // Darkened blend: overwrite but surface will be color-modulated later
359 dst_data[idx] = src_pixel;
360 break;
361
363 // Layer hidden - should not reach here due to early return
364 break;
365 }
366 }
367 };
368
369 // Process all layers in back-to-front order (matching SNES hardware)
370 // BG2 is the lower/background layer, BG1 is the upper/foreground layer.
371 CompositeLayer(bg2_layout, LayerType::BG2_Layout);
372 CompositeLayer(bg2_objects, LayerType::BG2_Objects);
373 CompositeLayer(bg1_layout, LayerType::BG1_Layout);
374 CompositeLayer(bg1_objects, LayerType::BG1_Objects);
375 }
376
377 // If no palette was copied from layers, try to get it from bg1_buffer directly
378 if (!palette_copied) {
379 const auto& bg1_bitmap = room.bg1_buffer().bitmap();
380 if (bg1_bitmap.surface()) {
381 ApplySDLPaletteToBitmap(bg1_bitmap.surface(), output);
382 }
383 }
384
385 // Sync pixel data to SDL surface for texture creation
386 output.UpdateSurfacePixels();
387
388 // Set up transparency and effects for the composite output
389 if (output.surface()) {
390 // IMPORTANT: Use the same transparency setup as room.cc's set_dungeon_palette
391 // Color key on index 255 (unused in 90-color dungeon palette)
392 SDL_SetColorKey(output.surface(), SDL_TRUE, 255);
393 SDL_SetSurfaceBlendMode(output.surface(), SDL_BLENDMODE_BLEND);
394
395 // Apply DarkRoom effect if merge type is 0x08
396 // This simulates the SNES master brightness reduction for unlit rooms
397 if (current_merge_type_id_ == 0x08) {
398 // Apply color modulation to darken the output (50% brightness)
399 SDL_SetSurfaceColorMod(output.surface(), 128, 128, 128);
400 } else {
401 // Reset to full brightness for non-dark rooms
402 SDL_SetSurfaceColorMod(output.surface(), 255, 255, 255);
403 }
404 }
405
406 // Mark output as modified for texture update
407 output.set_modified(true);
408}
409
410} // namespace zelda3
411} // namespace yaze
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:67
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
void UpdateSurfacePixels()
Update SDL surface with current pixel data from data_ vector Call this after modifying pixel data via...
Definition bitmap.cc:369
void set_modified(bool modified)
Definition bitmap.h:388
int height() const
Definition bitmap.h:374
void Fill(uint8_t value)
Fill the bitmap with a specific value.
Definition bitmap.h:143
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:384
int width() const
Definition bitmap.h:373
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:378
SDL_Surface * surface() const
Definition bitmap.h:379
static gfx::BackgroundBuffer & GetLayerBuffer(Room &room, LayerType layer)
Get the bitmap buffer for a layer type.
static bool IsTransparent(uint8_t pixel)
Check if a pixel index represents transparency.
bool IsLayerVisible(LayerType layer) const
LayerBlendMode GetLayerBlendMode(LayerType layer) const
void CompositeToOutput(Room &room, gfx::Bitmap &output) const
Composite all visible layers into a single output bitmap.
uint8_t GetLayerAlpha(LayerType layer) const
auto & bg1_buffer()
Definition room.h:630
int id() const
Definition room.h:566
#define LOG_DEBUG(category, format,...)
Definition log.h:103
void ApplySDLPaletteToBitmap(SDL_Surface *src_surface, gfx::Bitmap &dst_bitmap)
LayerBlendMode
Layer blend modes for compositing.
LayerType
Layer types for the 4-way visibility system.