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
135 if (room.layer2_mode() == 0x06) {
136 // Header layer mode 6 uses the upper dungeon tilemap on the SNES main
137 // screen and the lower tilemap on the sub screen. Ignoring OBJ/BG3, an
138 // opaque upper pixel wins regardless of either tile's priority bit; the
139 // lower pixel is visible only where the upper tilemap is transparent.
140 //
141 // Yaze's historic BG1/BG2 names describe semantic editor layers here:
142 // BG1_* is the upper tilemap ($7E2000 / hardware BG2), while BG2_* is the
143 // lower tilemap ($7E4000 / hardware BG1).
144 if (bg1_layout_on) {
145 CopyPaletteIfNeeded(bg1_layout.bitmap());
146 }
147 if (bg1_obj_on) {
148 CopyPaletteIfNeeded(bg1_objects.bitmap());
149 }
150 if (bg2_layout_on) {
151 CopyPaletteIfNeeded(bg2_layout.bitmap());
152 }
153 if (bg2_obj_on) {
154 CopyPaletteIfNeeded(bg2_objects.bitmap());
155 }
156
157 const auto& upper_layout_px = bg1_layout.bitmap().data();
158 const auto& upper_object_px = bg1_objects.bitmap().data();
159 const auto& lower_layout_px = bg2_layout.bitmap().data();
160 const auto& lower_object_px = bg2_objects.bitmap().data();
161 const auto& upper_object_coverage = bg1_objects.coverage_data();
162 const auto& lower_object_coverage = bg2_objects.coverage_data();
163
164 auto resolve_tilemap_pixel =
165 [&](bool layout_on, bool objects_on, const uint8_t* layout_pixels,
166 const uint8_t* object_pixels,
167 const std::vector<uint8_t>& object_coverage, int index) -> uint8_t {
168 if (objects_on) {
169 const bool object_wrote =
170 (index < static_cast<int>(object_coverage.size()) &&
171 object_coverage[index] != 0) ||
172 !IsTransparent(object_pixels[index]);
173 if (object_wrote) {
174 return object_pixels[index];
175 }
176 }
177 return layout_on ? layout_pixels[index] : 255;
178 };
179
180 auto& dst_data = output.mutable_data();
181 for (int idx = 0; idx < kPixelCount; ++idx) {
182 const uint8_t upper_pixel =
183 resolve_tilemap_pixel(bg1_layout_on, bg1_obj_on, upper_layout_px,
184 upper_object_px, upper_object_coverage, idx);
185 const uint8_t lower_pixel =
186 resolve_tilemap_pixel(bg2_layout_on, bg2_obj_on, lower_layout_px,
187 lower_object_px, lower_object_coverage, idx);
188
189 if (!IsTransparent(upper_pixel)) {
190 dst_data[idx] = upper_pixel;
191 } else if (!IsTransparent(lower_pixel)) {
192 dst_data[idx] = lower_pixel;
193 }
194 }
196 // Priority compositing (SNES Mode 1):
197 // - BG2 priority=0 is behind BG1 priority=0.
198 // - BG2 priority=1 can appear above BG1 priority=0.
199 // - BG1 priority=1 is above BG2 priority=1.
200 //
201 // We first combine Layout+Objects for each BG (objects overwrite layout),
202 // then resolve BG1 vs BG2 per-pixel using the stored priority buffers.
203 // Mode 7 needs palette-aware full addition even with tile priority off.
204 // In that case rank_for below retains the simple upper-over-lower order.
205 // Other translucent modes retain the existing half-add approximation.
206 const bool full_add_color_math = current_merge_type_id_ == 0x07;
207 // Ensure the output palette matches the room's SDL palette.
208 if (layer_enabled(LayerType::BG1_Layout, bg1_layout)) {
209 CopyPaletteIfNeeded(bg1_layout.bitmap());
210 }
211 if (layer_enabled(LayerType::BG1_Objects, bg1_objects)) {
212 CopyPaletteIfNeeded(bg1_objects.bitmap());
213 }
214 if (layer_enabled(LayerType::BG2_Layout, bg2_layout)) {
215 CopyPaletteIfNeeded(bg2_layout.bitmap());
216 }
217 if (layer_enabled(LayerType::BG2_Objects, bg2_objects)) {
218 CopyPaletteIfNeeded(bg2_objects.bitmap());
219 }
220
221 // Check if BG2 uses translucent blending (water rooms, color math effects).
222 // The editor maps color-math results back into the indexed palette. This
223 // remains an approximation when the resulting RGB color is absent.
224 const bool bg2_layout_translucent =
225 bg2_layout_on &&
227 const bool bg2_objects_translucent =
230
231 // Build palette lookup table for color blending (only when needed).
232 // Extract from the output bitmap's SDL palette so we can do RGB math.
233 std::vector<SDL_Color> pal_lut;
234 if ((bg2_layout_translucent || bg2_objects_translucent) &&
235 output.surface() && output.surface()->format &&
236 output.surface()->format->palette) {
237 SDL_Palette* sdl_pal = output.surface()->format->palette;
238 int n = std::min(sdl_pal->ncolors, 256);
239 pal_lut.resize(256, {0, 0, 0, 0});
240 for (int i = 0; i < n; ++i) {
241 pal_lut[i] = sdl_pal->colors[i];
242 }
243 }
244
245 // Nearest-color lookup for blended result.
246 // Searches within the same 16-color bank as the winning pixel to preserve
247 // palette coherence (avoids cross-bank color artifacts).
248 auto find_nearest_in_bank = [&](uint8_t base_idx, uint8_t r, uint8_t g,
249 uint8_t b) -> uint8_t {
250 if (pal_lut.empty())
251 return base_idx;
252 int bank_start = (base_idx / 16) * 16;
253 int bank_end = bank_start + 16;
254 int best_idx = base_idx;
255 int best_dist = INT_MAX;
256 for (int i = bank_start + 1; i < bank_end && i < 256; ++i) {
257 int dr = static_cast<int>(pal_lut[i].r) - r;
258 int dg = static_cast<int>(pal_lut[i].g) - g;
259 int db = static_cast<int>(pal_lut[i].b) - b;
260 int dist = dr * dr + dg * dg + db * db;
261 if (dist < best_dist) {
262 best_dist = dist;
263 best_idx = i;
264 }
265 }
266 return static_cast<uint8_t>(best_idx);
267 };
268
269 // Cache resolved blended indices to avoid repeating nearest-bank searches
270 // for the same winner/other palette pair in this frame.
271 std::array<uint8_t, 256 * 256> blend_cache{};
272 std::array<uint8_t, 256 * 256> blend_cache_valid{};
273 auto blend_channel = [&](uint8_t first, uint8_t second) -> uint8_t {
274 if (!full_add_color_math) {
275 return static_cast<uint8_t>((first + second) / 2);
276 }
277 // USDASM $02:A20C selects CGADSUB=$32 for mode 7 (full add),
278 // whereas $02:A212 selects $62 for mode 4 (half add). Saturate in
279 // five-bit color space, then expand to the room's SDL palette format.
280 const int sum = std::min(31, (first >> 3) + (second >> 3));
281 return static_cast<uint8_t>((sum << 3) | (sum >> 2));
282 };
283 auto resolve_blended_index = [&](uint8_t winner_idx,
284 uint8_t other_idx) -> uint8_t {
285 if (pal_lut.empty() ||
286 (!full_add_color_math && winner_idx == other_idx)) {
287 return winner_idx;
288 }
289 const size_t key = (static_cast<size_t>(winner_idx) << 8) |
290 static_cast<size_t>(other_idx);
291 if (blend_cache_valid[key] != 0) {
292 return blend_cache[key];
293 }
294
295 const SDL_Color& c1 = pal_lut[winner_idx];
296 const SDL_Color& c2 = pal_lut[other_idx];
297 const uint8_t blend_r = blend_channel(c1.r, c2.r);
298 const uint8_t blend_g = blend_channel(c1.g, c2.g);
299 const uint8_t blend_b = blend_channel(c1.b, c2.b);
300 const uint8_t resolved =
301 find_nearest_in_bank(winner_idx, blend_r, blend_g, blend_b);
302 blend_cache[key] = resolved;
303 blend_cache_valid[key] = 1;
304 return resolved;
305 };
306
307 auto normalize_pri = [](uint8_t pri) -> uint8_t {
308 // 0xFF is our "unset" value. Treat unset as low priority.
309 return (pri == 0xFF) ? 0 : (pri ? 1 : 0);
310 };
311
312 auto rank_for = [&](bool is_bg1, uint8_t pri) -> int {
313 pri = use_priority_compositing_ ? normalize_pri(pri) : 0;
314 if (is_bg1) {
315 return pri ? 3 : 1;
316 }
317 return pri ? 2 : 0;
318 };
319
320 const auto& bg1_layout_px = bg1_layout.bitmap().data();
321 const auto& bg1_obj_px = bg1_objects.bitmap().data();
322 const auto& bg2_layout_px = bg2_layout.bitmap().data();
323 const auto& bg2_obj_px = bg2_objects.bitmap().data();
324 const auto& bg1_layout_pri = bg1_layout.priority_data();
325 const auto& bg1_obj_pri = bg1_objects.priority_data();
326 const auto& bg2_layout_pri = bg2_layout.priority_data();
327 const auto& bg2_obj_pri = bg2_objects.priority_data();
328 const auto& bg1_obj_cov = bg1_objects.coverage_data();
329 const auto& bg2_obj_cov = bg2_objects.coverage_data();
330
331 auto& dst_data = output.mutable_data();
332 for (int idx = 0; idx < kPixelCount; ++idx) {
333 uint8_t bg1_pixel = 255;
334 uint8_t bg1_pri = 0;
335 const bool bg1_obj_wrote =
336 bg1_obj_on && ((idx < static_cast<int>(bg1_obj_cov.size()) &&
337 bg1_obj_cov[idx] != 0) ||
338 !IsTransparent(bg1_obj_px[idx]));
339 if (bg1_obj_wrote) {
340 if (!bg1_revealed_at(bg1_objects, idx)) {
341 bg1_pixel = bg1_obj_px[idx];
342 bg1_pri = bg1_obj_pri[idx];
343 }
344 } else if (bg1_layout_on && !IsTransparent(bg1_layout_px[idx])) {
345 if (!bg1_revealed_at(bg1_layout, idx)) {
346 bg1_pixel = bg1_layout_px[idx];
347 bg1_pri = bg1_layout_pri[idx];
348 }
349 }
350
351 uint8_t bg2_pixel = 255;
352 uint8_t bg2_pri = 0;
353 bool bg2_pixel_translucent = false;
354 const bool bg2_obj_wrote =
355 bg2_obj_on && ((idx < static_cast<int>(bg2_obj_cov.size()) &&
356 bg2_obj_cov[idx] != 0) ||
357 !IsTransparent(bg2_obj_px[idx]));
358 if (bg2_obj_wrote) {
359 bg2_pixel = bg2_obj_px[idx];
360 bg2_pri = bg2_obj_pri[idx];
361 bg2_pixel_translucent = bg2_objects_translucent;
362 } else if (bg2_layout_on && !IsTransparent(bg2_layout_px[idx])) {
363 bg2_pixel = bg2_layout_px[idx];
364 bg2_pri = bg2_layout_pri[idx];
365 bg2_pixel_translucent = bg2_layout_translucent;
366 }
367
368 if (IsTransparent(bg1_pixel)) {
369 if (!IsTransparent(bg2_pixel)) {
370 dst_data[idx] = bg2_pixel;
371 }
372 continue;
373 }
374 if (IsTransparent(bg2_pixel)) {
375 dst_data[idx] = bg1_pixel;
376 continue;
377 }
378
379 // Both layers have opaque pixels. Resolve using priority + blend mode.
380 const int r1 = rank_for(/*is_bg1=*/true, bg1_pri);
381 const int r2 = rank_for(/*is_bg1=*/false, bg2_pri);
382
383 // Resolve overlapping colors using mode 7 full add or the existing
384 // half-add approximation. Indexed output still quantizes to a palette.
385 // Blend only the selected BG2 source. Hidden or overwritten sources do
386 // not participate in this pixel's color math.
387 if (bg2_pixel_translucent && !pal_lut.empty()) {
388 const bool bg1_wins = (r1 >= r2);
389 const uint8_t winner = bg1_wins ? bg1_pixel : bg2_pixel;
390 const uint8_t other = bg1_wins ? bg2_pixel : bg1_pixel;
391 dst_data[idx] = resolve_blended_index(winner, other);
392 } else {
393 dst_data[idx] = (r1 >= r2) ? bg1_pixel : bg2_pixel;
394 }
395 }
396 } else {
397 // Fallback to simple back-to-front layer order: BG2 then BG1.
398 //
399 // Blend Modes (from LayerMergeType):
400 // - Normal: Opaque pixels overwrite destination (standard)
401 // - Translucent: 50% alpha blend with destination
402 // - Addition: Additive color blending (SNES color math)
403 // - Dark: Darkened blend (reduced brightness)
404 // - Off: Layer is hidden
405 //
406 // Transparent BG1 pixels and active source-owned reveal bits expose BG2.
407 // The deferred bits preserve raw BG1 when their owning BG2 layer is hidden.
408 auto CompositeLayer = [&](gfx::BackgroundBuffer& buffer,
409 LayerType layer_type) {
410 if (!layer_enabled(layer_type, buffer))
411 return;
412
413 const auto& src_bitmap = buffer.bitmap();
414 LayerBlendMode blend_mode = GetLayerBlendMode(layer_type);
415
416 CopyPaletteIfNeeded(src_bitmap);
417
418 const auto& src_data = src_bitmap.data();
419 auto& dst_data = output.mutable_data();
420
421 // Get layer alpha for translucent/dark blending
422 uint8_t layer_alpha = GetLayerAlpha(layer_type);
423
424 for (int idx = 0; idx < kPixelCount; ++idx) {
425 const bool is_bg1_layer = layer_type == LayerType::BG1_Layout ||
426 layer_type == LayerType::BG1_Objects;
427 if (is_bg1_layer && bg1_revealed_at(buffer, idx)) {
428 continue;
429 }
430 uint8_t src_pixel = src_data[idx];
431
432 // Skip transparent pixels (255 = fill color for undrawn areas).
433 if (IsTransparent(src_pixel))
434 continue;
435
436 // Apply blend mode
437 switch (blend_mode) {
439 // Standard opaque overwrite
440 dst_data[idx] = src_pixel;
441 break;
442
444 // Fallback alpha approximation for paths without the palette-aware
445 // priority compositor above. This is not a pixel-exact SNES model.
446 if (IsTransparent(dst_data[idx]) || layer_alpha > 180) {
447 dst_data[idx] = src_pixel;
448 }
449 // If layer_alpha <= 180, destination shows through (simplified blend)
450 break;
451
453 // Additive blending: in indexed mode, just use source if visible
454 // True additive would need RGB values from palette
455 if (IsTransparent(dst_data[idx])) {
456 dst_data[idx] = src_pixel;
457 } else {
458 dst_data[idx] = src_pixel;
459 }
460 break;
461
463 // Darkened blend: overwrite but surface will be color-modulated later
464 dst_data[idx] = src_pixel;
465 break;
466
468 // Layer hidden - should not reach here due to early return
469 break;
470 }
471 }
472 };
473
474 // Process all layers in back-to-front order (matching SNES hardware)
475 // BG2 is the lower/background layer, BG1 is the upper/foreground layer.
476 CompositeLayer(bg2_layout, LayerType::BG2_Layout);
477 CompositeLayer(bg2_objects, LayerType::BG2_Objects);
478 CompositeLayer(bg1_layout, LayerType::BG1_Layout);
479 CompositeLayer(bg1_objects, LayerType::BG1_Objects);
480 }
481
482 // If no palette was copied from layers, try to get it from bg1_buffer directly
483 if (!palette_copied) {
484 const auto& bg1_bitmap = room.bg1_buffer().bitmap();
485 if (bg1_bitmap.surface()) {
486 ApplySDLPaletteToBitmap(bg1_bitmap.surface(), output);
487 }
488 }
489
490 // Sync pixel data to SDL surface for texture creation
491 output.UpdateSurfacePixels();
492
493 // Set up transparency and effects for the composite output
494 if (output.surface()) {
495 // IMPORTANT: Use the same transparency setup as room.cc's set_dungeon_palette
496 // Color key on index 255 (unused in 90-color dungeon palette)
497 SDL_SetColorKey(output.surface(), SDL_TRUE, 255);
498 SDL_SetSurfaceBlendMode(output.surface(), SDL_BLENDMODE_BLEND);
499
500 // Apply DarkRoom effect if merge type is 0x08
501 // This simulates the SNES master brightness reduction for unlit rooms
502 if (current_merge_type_id_ == 0x08) {
503 // Apply color modulation to darken the output (50% brightness)
504 SDL_SetSurfaceColorMod(output.surface(), 128, 128, 128);
505 } else {
506 // Reset to full brightness for non-dark rooms
507 SDL_SetSurfaceColorMod(output.surface(), 255, 255, 255);
508 }
509 }
510
511 // Mark output as modified for texture update
512 output.set_modified(true);
513}
514
515} // namespace zelda3
516} // namespace yaze
const std::vector< uint8_t > & bg1_reveal_mask_data() const
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:69
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:202
void UpdateSurfacePixels()
Update SDL surface with current pixel data from data_ vector Call this after modifying pixel data via...
Definition bitmap.cc:379
void set_modified(bool modified)
Definition bitmap.h:411
int height() const
Definition bitmap.h:397
void Fill(uint8_t value)
Fill the bitmap with a specific value.
Definition bitmap.h:145
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:394
int width() const
Definition bitmap.h:396
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:401
SDL_Surface * surface() const
Definition bitmap.h:402
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
uint8_t layer2_mode() const
Definition room.h:940
auto & bg1_buffer()
Definition room.h:1039
int id() const
Definition room.h:948
#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.