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