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 <vector>
6
7#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 // Get all 4 layer buffers
78 auto& bg1_layout = GetLayerBuffer(room, LayerType::BG1_Layout);
79 auto& bg1_objects = GetLayerBuffer(room, LayerType::BG1_Objects);
80 auto& bg2_layout = GetLayerBuffer(room, LayerType::BG2_Layout);
81 auto& bg2_objects = GetLayerBuffer(room, LayerType::BG2_Objects);
82
83 auto layer_enabled = [&](LayerType type,
84 const gfx::BackgroundBuffer& buffer) {
85 if (!IsLayerVisible(type) || IsHiddenByGame(type) ||
87 return false;
88 }
89 const auto& bitmap = buffer.bitmap();
90 return bitmap.is_active() && bitmap.width() > 0;
91 };
92
93 const bool bg1_layout_on = layer_enabled(LayerType::BG1_Layout, bg1_layout);
94 const bool bg1_obj_on = layer_enabled(LayerType::BG1_Objects, bg1_objects);
95 const bool bg2_layout_on = layer_enabled(LayerType::BG2_Layout, bg2_layout);
96 const bool bg2_obj_on = layer_enabled(LayerType::BG2_Objects, bg2_objects);
97
98 const bool game_color_math =
100 const bool bg2_layout_translucent =
101 bg2_layout_on &&
103 const bool bg2_objects_translucent =
104 bg2_obj_on &&
106 const bool true_color = !UpperTilemapCoversLower(room.layer2_mode()) &&
108 current_merge_type_id_ == 7 || game_color_math) &&
109 (bg2_layout_translucent || bg2_objects_translucent);
110 const int depth = true_color ? 32 : 8;
111 if (output.width() != kWidth || output.height() != kHeight ||
112 output.depth() != depth) {
113 output.Create(kWidth, kHeight, depth,
114 std::vector<uint8_t>(kPixelCount * (true_color ? 4 : 1), 0));
115 } else {
116 output.Fill(0);
117 }
119 // Preserve source indices; only the presentation image becomes RGBA.
120 bool palette_copied = false;
121 std::array<SDL_Color, 256> pal_lut{};
122 pal_lut.fill({0, 0, 0, 255});
123 auto bg1_revealed_at = [&](const gfx::BackgroundBuffer& target, int index) {
124 // Water objects' legacy reveal masks emulate opaque cross-layer drawing.
125 // Main/subscreen color math needs both tilemaps intact. Actual transparent
126 // writes still replace their own tilemap through the coverage buffers.
127 if (game_color_math) {
128 return false;
129 }
130 const auto& mask = target.bg1_reveal_mask_data();
131 if (index < 0 || index >= static_cast<int>(mask.size())) {
132 return false;
133 }
134 const uint8_t value = mask[index];
135 const bool layout_reveal =
136 bg2_layout_on &&
137 (value & static_cast<uint8_t>(gfx::BG1RevealMaskSource::kBG2Layout)) !=
138 0;
139 const bool object_reveal =
140 bg2_obj_on &&
141 (value & static_cast<uint8_t>(gfx::BG1RevealMaskSource::kBG2Objects)) !=
142 0;
143 return layout_reveal || object_reveal;
144 };
145
146 // Copy palette from first available visible layer
147 auto CopyPaletteIfNeeded = [&](const gfx::Bitmap& src_bitmap) {
148 if (!palette_copied && src_bitmap.surface()) {
149 if (const auto* palette = src_bitmap.surface()->format->palette) {
150 std::copy_n(palette->colors, std::min(palette->ncolors, 256),
151 pal_lut.begin());
152 }
153 pal_lut[0] = {0, 0, 0, 255};
154 pal_lut[255] = {0, 0, 0, 0};
155 if (!true_color)
156 ApplySDLPaletteToBitmap(src_bitmap.surface(), output);
157 palette_copied = true;
158 }
159 };
160
162 // Header layer mode 6 uses the upper dungeon tilemap on the SNES main
163 // screen and the lower tilemap on the sub screen. Ignoring OBJ/BG3, an
164 // opaque upper pixel wins regardless of either tile's priority bit; the
165 // lower pixel is visible only where the upper tilemap is transparent.
166 //
167 // Yaze's historic BG1/BG2 names describe semantic editor layers here:
168 // BG1_* is the upper tilemap ($7E2000 / hardware BG2), while BG2_* is the
169 // lower tilemap ($7E4000 / hardware BG1).
170 if (bg1_layout_on) {
171 CopyPaletteIfNeeded(bg1_layout.bitmap());
172 }
173 if (bg1_obj_on) {
174 CopyPaletteIfNeeded(bg1_objects.bitmap());
175 }
176 if (bg2_layout_on) {
177 CopyPaletteIfNeeded(bg2_layout.bitmap());
178 }
179 if (bg2_obj_on) {
180 CopyPaletteIfNeeded(bg2_objects.bitmap());
181 }
182
183 const auto& upper_layout_px = bg1_layout.bitmap().data();
184 const auto& upper_object_px = bg1_objects.bitmap().data();
185 const auto& lower_layout_px = bg2_layout.bitmap().data();
186 const auto& lower_object_px = bg2_objects.bitmap().data();
187 const auto& upper_object_coverage = bg1_objects.coverage_data();
188 const auto& lower_object_coverage = bg2_objects.coverage_data();
189
190 auto resolve_tilemap_pixel =
191 [&](bool layout_on, bool objects_on, const uint8_t* layout_pixels,
192 const uint8_t* object_pixels,
193 const std::vector<uint8_t>& object_coverage, int index) -> uint8_t {
194 if (objects_on) {
195 const bool object_wrote =
196 (index < static_cast<int>(object_coverage.size()) &&
197 object_coverage[index] != 0) ||
198 !IsTransparent(object_pixels[index]);
199 if (object_wrote) {
200 return object_pixels[index];
201 }
202 }
203 return layout_on ? layout_pixels[index] : 255;
204 };
205
206 auto& dst_data = output.mutable_data();
207 for (int idx = 0; idx < kPixelCount; ++idx) {
208 const uint8_t upper_pixel =
209 resolve_tilemap_pixel(bg1_layout_on, bg1_obj_on, upper_layout_px,
210 upper_object_px, upper_object_coverage, idx);
211 const uint8_t lower_pixel =
212 resolve_tilemap_pixel(bg2_layout_on, bg2_obj_on, lower_layout_px,
213 lower_object_px, lower_object_coverage, idx);
214
215 if (!IsTransparent(upper_pixel)) {
216 dst_data[idx] = upper_pixel;
217 } else if (!IsTransparent(lower_pixel)) {
218 dst_data[idx] = lower_pixel;
219 }
220 }
222 game_color_math) {
223 // Priority compositing (SNES Mode 1):
224 // - BG2 priority=0 is behind BG1 priority=0.
225 // - BG2 priority=1 can appear above BG1 priority=0.
226 // - BG1 priority=1 is above BG2 priority=1.
227 //
228 // We first combine Layout+Objects for each BG (objects overwrite layout),
229 // then resolve BG1 vs BG2 per-pixel using the stored priority buffers.
230 // Game color math and mode 7 still blend with tile priority turned off.
231 // In that case rank_for below retains the simple upper-over-lower order.
232 // CGADSUB arithmetic is in five-bit channels, before display expansion.
233 const bool subtract = game_registers_applied_ && (cgadsub_ & 0x80);
234 const bool halve = game_registers_applied_ ? (cgadsub_ & 0x40) != 0
235 : current_merge_type_id_ != 0x07;
236 // Ensure the output palette matches the room's SDL palette.
237 if (layer_enabled(LayerType::BG1_Layout, bg1_layout)) {
238 CopyPaletteIfNeeded(bg1_layout.bitmap());
239 }
240 if (layer_enabled(LayerType::BG1_Objects, bg1_objects)) {
241 CopyPaletteIfNeeded(bg1_objects.bitmap());
242 }
243 if (layer_enabled(LayerType::BG2_Layout, bg2_layout)) {
244 CopyPaletteIfNeeded(bg2_layout.bitmap());
245 }
246 if (layer_enabled(LayerType::BG2_Objects, bg2_objects)) {
247 CopyPaletteIfNeeded(bg2_objects.bitmap());
248 }
249
250 auto write_color = [&](int pixel, SDL_Color color) {
251 auto* dst = output.mutable_data().data() + pixel * 4;
252 dst[0] = color.r;
253 dst[1] = color.g;
254 dst[2] = color.b;
255 dst[3] = 255;
256 };
257 auto write_index = [&](int pixel, uint8_t index) {
258 if (true_color) {
259 write_color(pixel, pal_lut[index]);
260 } else {
261 output.mutable_data()[pixel] = index;
262 }
263 };
264 auto blend_color = [&](uint8_t main_index, uint8_t sub_index) {
265 const auto& main = pal_lut[main_index];
266 const auto& sub = pal_lut[sub_index];
267 const auto channel = [&](uint8_t m, uint8_t s) {
269 gfx::SnesColorMathChannel(m >> 3, s >> 3, subtract, halve));
270 };
271 return SDL_Color{channel(main.r, sub.r), channel(main.g, sub.g),
272 channel(main.b, sub.b), 255};
273 };
274
275 auto normalize_pri = [](uint8_t pri) -> uint8_t {
276 // 0xFF is our "unset" value. Treat unset as low priority.
277 return (pri == 0xFF) ? 0 : (pri ? 1 : 0);
278 };
279
280 // Mode 1 order: hardware BG1 p1 > BG2 p1 > BG1 p0 > BG2 p0. When both
281 // tilemaps are on the main screen the lower one is hardware BG1 and wins
282 // ties; otherwise keep the historic upper-first order.
283 const bool lower_wins_ties = LowerTilemapWinsTies();
284 auto rank_for = [&](bool is_bg1, uint8_t pri) -> int {
285 pri = use_priority_compositing_ ? normalize_pri(pri) : 0;
286 const bool first = lower_wins_ties ? !is_bg1 : is_bg1;
287 if (first) {
288 return pri ? 3 : 1;
289 }
290 return pri ? 2 : 0;
291 };
292
293 const auto& bg1_layout_px = bg1_layout.bitmap().data();
294 const auto& bg1_obj_px = bg1_objects.bitmap().data();
295 const auto& bg2_layout_px = bg2_layout.bitmap().data();
296 const auto& bg2_obj_px = bg2_objects.bitmap().data();
297 const auto& bg1_layout_pri = bg1_layout.priority_data();
298 const auto& bg1_obj_pri = bg1_objects.priority_data();
299 const auto& bg2_layout_pri = bg2_layout.priority_data();
300 const auto& bg2_obj_pri = bg2_objects.priority_data();
301 const auto& bg1_obj_cov = bg1_objects.coverage_data();
302 const auto& bg2_obj_cov = bg2_objects.coverage_data();
303
304 for (int idx = 0; idx < kPixelCount; ++idx) {
305 write_index(idx, 0);
306 uint8_t bg1_pixel = 255;
307 uint8_t bg1_pri = 0;
308 const bool bg1_obj_wrote =
309 bg1_obj_on && ((idx < static_cast<int>(bg1_obj_cov.size()) &&
310 bg1_obj_cov[idx] != 0) ||
311 !IsTransparent(bg1_obj_px[idx]));
312 if (bg1_obj_wrote) {
313 if (!bg1_revealed_at(bg1_objects, idx)) {
314 bg1_pixel = bg1_obj_px[idx];
315 bg1_pri = bg1_obj_pri[idx];
316 }
317 } else if (bg1_layout_on && !IsTransparent(bg1_layout_px[idx])) {
318 if (!bg1_revealed_at(bg1_layout, idx)) {
319 bg1_pixel = bg1_layout_px[idx];
320 bg1_pri = bg1_layout_pri[idx];
321 }
322 }
323
324 uint8_t bg2_pixel = 255;
325 uint8_t bg2_pri = 0;
326 bool bg2_pixel_translucent = false;
327 const bool bg2_obj_wrote =
328 bg2_obj_on && ((idx < static_cast<int>(bg2_obj_cov.size()) &&
329 bg2_obj_cov[idx] != 0) ||
330 !IsTransparent(bg2_obj_px[idx]));
331 if (bg2_obj_wrote) {
332 bg2_pixel = bg2_obj_px[idx];
333 bg2_pri = bg2_obj_pri[idx];
334 bg2_pixel_translucent = bg2_objects_translucent;
335 } else if (bg2_layout_on && !IsTransparent(bg2_layout_px[idx])) {
336 bg2_pixel = bg2_layout_px[idx];
337 bg2_pri = bg2_layout_pri[idx];
338 bg2_pixel_translucent = bg2_layout_translucent;
339 }
340
341 if (IsTransparent(bg1_pixel)) {
342 if (!IsTransparent(bg2_pixel)) {
343 if (game_color_math && (cgadsub_ & 0x20) && bg2_pixel_translucent &&
344 (bg1_layout_on || bg1_obj_on)) {
345 // A transparent main tile exposes the backdrop, which CGADSUB
346 // can also blend. Explicitly hiding the main layers is an editor
347 // inspection view and continues to show the sub layer alone.
348 write_color(idx, blend_color(0, bg2_pixel));
349 } else {
350 write_index(idx, bg2_pixel);
351 }
352 }
353 continue;
354 }
355 if (IsTransparent(bg2_pixel)) {
356 write_index(idx, bg1_pixel);
357 continue;
358 }
359
360 // Both layers have opaque pixels. Resolve using priority + blend mode.
361 const int r1 = rank_for(/*is_bg1=*/true, bg1_pri);
362 const int r2 = rank_for(/*is_bg1=*/false, bg2_pri);
363
364 // Upper tilemap is hardware BG2 on the main screen. Subtraction must
365 // preserve that operand order even when inspecting tile priorities.
366 // Hidden or overwritten BG2 sources never participate in color math.
367 if (bg2_pixel_translucent && true_color) {
368 write_color(idx, blend_color(bg1_pixel, bg2_pixel));
369 } else {
370 write_index(idx, (r1 >= r2) ? bg1_pixel : bg2_pixel);
371 }
372 }
373 } else {
374 // Fallback to simple back-to-front layer order: BG2 then BG1.
375 //
376 // Blend Modes (from LayerMergeType):
377 // - Normal: Opaque pixels overwrite destination (standard)
378 // - Translucent: 50% alpha blend with destination
379 // - Addition: Additive color blending (SNES color math)
380 // - Dark: Darkened blend (reduced brightness)
381 // - Off: Layer is hidden
382 //
383 // Transparent BG1 pixels and active source-owned reveal bits expose BG2.
384 // The deferred bits preserve raw BG1 when their owning BG2 layer is hidden.
385 auto CompositeLayer = [&](gfx::BackgroundBuffer& buffer,
386 LayerType layer_type) {
387 if (!layer_enabled(layer_type, buffer))
388 return;
389
390 const auto& src_bitmap = buffer.bitmap();
391 LayerBlendMode blend_mode = GetLayerBlendMode(layer_type);
392
393 CopyPaletteIfNeeded(src_bitmap);
394
395 const auto& src_data = src_bitmap.data();
396 auto& dst_data = output.mutable_data();
397
398 // Get layer alpha for translucent/dark blending
399 uint8_t layer_alpha = GetLayerAlpha(layer_type);
400
401 for (int idx = 0; idx < kPixelCount; ++idx) {
402 const bool is_bg1_layer = layer_type == LayerType::BG1_Layout ||
403 layer_type == LayerType::BG1_Objects;
404 if (is_bg1_layer && bg1_revealed_at(buffer, idx)) {
405 continue;
406 }
407 uint8_t src_pixel = src_data[idx];
408
409 // Skip transparent pixels (255 = fill color for undrawn areas).
410 if (IsTransparent(src_pixel))
411 continue;
412
413 // Apply blend mode
414 switch (blend_mode) {
416 // Standard opaque overwrite
417 dst_data[idx] = src_pixel;
418 break;
419
421 // Fallback alpha approximation for paths without the palette-aware
422 // priority compositor above. This is not a pixel-exact SNES model.
423 if (IsTransparent(dst_data[idx]) || layer_alpha > 180) {
424 dst_data[idx] = src_pixel;
425 }
426 // If layer_alpha <= 180, destination shows through (simplified blend)
427 break;
428
430 // Additive blending: in indexed mode, just use source if visible
431 // True additive would need RGB values from palette
432 if (IsTransparent(dst_data[idx])) {
433 dst_data[idx] = src_pixel;
434 } else {
435 dst_data[idx] = src_pixel;
436 }
437 break;
438
440 // Darkened blend: overwrite but surface will be color-modulated later
441 dst_data[idx] = src_pixel;
442 break;
443
445 // Layer hidden - should not reach here due to early return
446 break;
447 }
448 }
449 };
450
451 // Process all layers in back-to-front order (matching SNES hardware)
452 // BG2 is the lower/background layer, BG1 is the upper/foreground layer.
453 CompositeLayer(bg2_layout, LayerType::BG2_Layout);
454 CompositeLayer(bg2_objects, LayerType::BG2_Objects);
455 CompositeLayer(bg1_layout, LayerType::BG1_Layout);
456 CompositeLayer(bg1_objects, LayerType::BG1_Objects);
457 }
458
459 // If no palette was copied from layers, try to get it from bg1_buffer directly
460 if (!palette_copied && !true_color) {
461 const auto& bg1_bitmap = room.bg1_buffer().bitmap();
462 if (bg1_bitmap.surface()) {
463 ApplySDLPaletteToBitmap(bg1_bitmap.surface(), output);
464 }
465 }
466
467 // Sync pixel data to SDL surface for texture creation
468 output.UpdateSurfacePixels();
469
470 // Set up transparency and effects for the composite output
471 if (output.surface()) {
472 // IMPORTANT: Use the same transparency setup as room.cc's set_dungeon_palette
473 // Color key on index 255 (unused in 90-color dungeon palette)
474 SDL_SetColorKey(output.surface(), true_color ? SDL_FALSE : SDL_TRUE, 255);
475 SDL_SetSurfaceBlendMode(output.surface(), SDL_BLENDMODE_BLEND);
476
477 // Apply DarkRoom effect if merge type is 0x08
478 // This simulates the SNES master brightness reduction for unlit rooms
479 if (current_merge_type_id_ == 0x08) {
480 // Apply color modulation to darken the output (50% brightness)
481 SDL_SetSurfaceColorMod(output.surface(), 128, 128, 128);
482 } else {
483 // Reset to full brightness for non-dark rooms
484 SDL_SetSurfaceColorMod(output.surface(), 255, 255, 255);
485 }
486 }
487
488 // Mark output as modified for texture update
489 output.set_modified(true);
490}
491
492} // namespace zelda3
493} // namespace yaze
const std::vector< uint8_t > & bg1_reveal_mask_data() const
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
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:198
void UpdateSurfacePixels()
Update SDL surface with current pixel data from data_ vector Call this after modifying pixel data via...
Definition bitmap.cc:373
BitmapMetadata & metadata()
Definition bitmap.h:395
void set_modified(bool modified)
Definition bitmap.h:418
int height() const
Definition bitmap.h:399
void Fill(uint8_t value)
Fill the bitmap with a specific value.
Definition bitmap.h:146
void SetPalette(const SnesPalette &palette)
Set the palette for the bitmap using SNES palette format.
Definition bitmap.cc:452
int width() const
Definition bitmap.h:398
int depth() const
Definition bitmap.h:400
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:408
SDL_Surface * surface() const
Definition bitmap.h:409
bool IsHiddenByGame(LayerType layer) const
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
bool UpperTilemapCoversLower(int layer2_mode) const
uint8_t layer2_mode() const
Definition room.h:991
auto & bg1_buffer()
Definition room.h:1145
int id() const
Definition room.h:1012
int main(int argc, char **argv)
Definition emu.cc:43
#define LOG_DEBUG(category, format,...)
Definition log.h:133
constexpr uint8_t SnesColorMathChannel(uint8_t main, uint8_t sub, bool subtract, bool half)
constexpr uint8_t ExpandSnesChannel(uint8_t value)
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.