yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
render_service.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cmath>
5#include <cstdint>
6#include <mutex>
7#include <vector>
8
9#include "absl/status/status.h"
10#include "absl/strings/numbers.h"
11#include "absl/strings/str_format.h"
12#include "app/gfx/core/bitmap.h"
15#include "util/macro.h"
17#include "zelda3/dungeon/room.h"
23
24#include <SDL.h>
25#ifdef YAZE_CLI_HAS_PNG
26#include <png.h>
27#endif
28
29namespace yaze {
30namespace app {
31namespace service {
32
33namespace {
34
35absl::Status ValidateRenderScale(float scale) {
36 if (!std::isfinite(scale) || scale < 0.25f || scale > 8.0f) {
37 return absl::InvalidArgumentError(
38 "Render scale must be finite and between 0.25 and 8.0");
39 }
40 return absl::OkStatus();
41}
42
43#ifdef YAZE_CLI_HAS_PNG
44// PNG write helpers (mirrored from visual_diff_engine.cc).
45struct PngCtx {
46 std::vector<uint8_t>* buf;
47};
48
49void PngWrite(png_structp png, png_bytep data, png_size_t len) {
50 auto* ctx = static_cast<PngCtx*>(png_get_io_ptr(png));
51 ctx->buf->insert(ctx->buf->end(), data, data + len);
52}
53
54void PngFlush(png_structp /*png*/) {}
55#endif
56
57// Returns the overlay RGBA color for a custom-collision tile value.
58// Returns alpha=0 for tiles we don't want to highlight.
59struct TileColor {
60 uint8_t r, g, b, a;
61};
62
64 if (tile == 0)
65 return {0, 0, 0, 0};
66 if (tile == 0xB0 || tile == 0xB1)
67 return {0, 210, 170, 150}; // straight — teal
68 if (tile >= 0xB2 && tile <= 0xB5)
69 return {0, 190, 255, 150}; // corner — cyan
70 if (tile == 0xB6)
71 return {255, 215, 0, 170}; // intersection — gold
72 if (tile >= 0xB7 && tile <= 0xBA)
73 return {255, 80, 0, 200}; // stop — orange-red
74 if (tile >= 0xBB && tile <= 0xBE)
75 return {80, 150, 255, 150}; // T-junction — blue
76 if (tile >= 0xD0 && tile <= 0xD3)
77 return {210, 0, 255, 170}; // switch corner — magenta
78 return {140, 140, 140, 120}; // other non-zero — grey
79}
80
81// 3x5 glyphs for overlay labels; each row is 3 bits, MSB = left column.
82const uint8_t* GlyphFor(char c) {
83 static const uint8_t kDigits[16][5] = {
84 {7, 5, 5, 5, 7}, {2, 6, 2, 2, 7}, {7, 1, 7, 4, 7}, {7, 1, 7, 1, 7},
85 {5, 5, 7, 1, 1}, {7, 4, 7, 1, 7}, {7, 4, 7, 5, 7}, {7, 1, 1, 2, 2},
86 {7, 5, 7, 5, 7}, {7, 5, 7, 1, 7}, {2, 5, 7, 5, 5}, {6, 5, 6, 5, 6},
87 {3, 4, 4, 4, 3}, {6, 5, 5, 5, 6}, {7, 4, 6, 4, 7}, {7, 4, 6, 4, 4}};
88 static const uint8_t kX[5] = {5, 5, 2, 5, 5};
89 static const uint8_t kH[5] = {5, 5, 7, 5, 5};
90 static const uint8_t kI[5] = {7, 2, 2, 2, 7};
91 static const uint8_t kColon[5] = {0, 2, 0, 2, 0};
92 if (c >= '0' && c <= '9')
93 return kDigits[c - '0'];
94 if (c >= 'A' && c <= 'F')
95 return kDigits[10 + (c - 'A')];
96 if (c == 'X')
97 return kX;
98 if (c == 'H')
99 return kH;
100 if (c == 'I')
101 return kI;
102 if (c == ':')
103 return kColon;
104 return nullptr;
105}
106
107// Draw a label with a dark backing box; font pixels are 2x2 area pixels.
108void DrawLabel(HeadlessOverlayRenderer& draw, float x, float y,
109 const std::string& text, uint8_t r, uint8_t g, uint8_t b) {
110 constexpr float kPx = 2.0f;
111 const float w = static_cast<float>(text.size()) * 4 * kPx + kPx;
112 const float h = 7 * kPx;
113 draw.DrawFilledRect(x, y, w, h, 0, 0, 0, 200);
114 for (size_t i = 0; i < text.size(); ++i) {
115 const uint8_t* glyph = GlyphFor(text[i]);
116 if (!glyph)
117 continue;
118 const float gx = x + kPx + static_cast<float>(i) * 4 * kPx;
119 for (int row = 0; row < 5; ++row) {
120 for (int col = 0; col < 3; ++col) {
121 if (glyph[row] & (4 >> col)) {
122 draw.DrawFilledRect(gx + col * kPx, y + kPx + row * kPx, kPx, kPx, r,
123 g, b, 255);
124 }
125 }
126 }
127 }
128}
129
131 uint8_t r, g, b;
132};
133
135 switch (phase) {
136 case 0:
137 return {80, 255, 120}; // beginning: green
138 case 1:
139 return {255, 80, 80}; // first part: red
140 default:
141 return {90, 170, 255}; // second part: blue
142 }
143}
144
145} // namespace
146
147absl::StatusOr<float> ParseRenderScale(absl::string_view value) {
148 float scale = 0;
149 if (!absl::SimpleAtof(value, &scale)) {
150 return absl::InvalidArgumentError(
151 absl::StrFormat("Invalid scale value: %s", value));
152 }
153 if (const auto status = ValidateRenderScale(scale); !status.ok()) {
154 return status;
155 }
156 return scale;
157}
158
160 : rom_(rom), game_data_(game_data) {}
161
162absl::StatusOr<RenderResult> RenderService::RenderDungeonRoom(
163 const RenderRequest& req) {
164 if (const auto status = ValidateRenderScale(req.scale); !status.ok()) {
165 return status;
166 }
167 if (!rom_ || !rom_->is_loaded()) {
168 return absl::FailedPreconditionError("ROM not loaded");
169 }
170 if (!game_data_) {
171 return absl::FailedPreconditionError("GameData not available");
172 }
173 if (req.room_id < 0 || req.room_id >= zelda3::kNumberOfRooms) {
174 return absl::InvalidArgumentError(
175 absl::StrFormat("Invalid room_id 0x%02X", req.room_id));
176 }
177
178#ifndef YAZE_CLI_HAS_PNG
179 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
180#endif
181
182 std::lock_guard<std::mutex> lock(mu_);
183
184 // Load room data from ROM (header, objects, pots, torches, blocks, pits).
187 if (!req.sheet_overrides.empty()) {
189 }
190
191 // Load sprites (requires ROM, game_data not needed here).
192 room.LoadSprites();
193
194 // Load graphics sheets and render tiles to BackgroundBuffers (CPU only).
195 room.LoadRoomGraphics();
196 room.RenderRoomGraphics();
197
198 // Composite all layers to a single Bitmap (CPU, SDL surface with palette).
199 zelda3::RoomLayerManager layer_mgr;
200 layer_mgr.ApplyLayerMerging(room.layer_merging());
201 layer_mgr.ApplyRoomEffect(room.effect());
203 auto& composite = room.GetCompositeBitmap(layer_mgr);
204
205 if (!composite.is_active() || composite.width() <= 0 ||
206 composite.height() <= 0) {
207 return absl::InternalError("Composite bitmap is empty after render");
208 }
209
210 const int out_w = static_cast<int>(composite.width() * req.scale);
211 const int out_h = static_cast<int>(composite.height() * req.scale);
212
213 // Convert indexed+palette bitmap to RGBA bytes.
214 auto rgba_or = BitmapToRgba(composite, out_w, out_h);
215 if (!rgba_or.ok())
216 return rgba_or.status();
217 auto rgba = std::move(rgba_or).value();
218
219 // Paint overlays directly into the RGBA buffer.
221 ApplyOverlays(rgba, out_w, out_h, room, req.overlay_flags, req.scale);
222 }
223
224 // Encode to PNG.
225 auto png_or = EncodePng(rgba, out_w, out_h);
226 if (!png_or.ok())
227 return png_or.status();
228
229 RenderResult result;
230 result.png_data = std::move(png_or).value();
231 result.width = out_w;
232 result.height = out_h;
233 return result;
234}
235
236absl::StatusOr<OverworldRenderResult> RenderService::RenderOverworldArea(
237 const OverworldRenderRequest& req) {
238 if (const auto status = ValidateRenderScale(req.scale); !status.ok()) {
239 return status;
240 }
241 if (!rom_ || !rom_->is_loaded()) {
242 return absl::FailedPreconditionError("ROM not loaded");
243 }
244 if (!game_data_) {
245 return absl::FailedPreconditionError("GameData not available");
246 }
247 if (req.screen_id < 0 || req.screen_id >= zelda3::kNumOverworldMaps) {
248 return absl::InvalidArgumentError(
249 absl::StrFormat("Invalid overworld screen 0x%02X", req.screen_id));
250 }
251 if (req.phase < -1 || req.phase > 2) {
252 return absl::InvalidArgumentError("Sprite phase must be 0, 1, or 2");
253 }
254
255#ifndef YAZE_CLI_HAS_PNG
256 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
257#endif
258
259 std::lock_guard<std::mutex> lock(mu_);
260
262 RETURN_IF_ERROR(overworld.Load(rom_));
263 if (!req.graphics_preview.empty()) {
265 }
266
267 const auto* requested = overworld.overworld_map(req.screen_id);
268 if (requested == nullptr) {
269 return absl::InternalError("Overworld map missing after load");
270 }
271 const int parent = requested->parent();
272 const auto* parent_map = overworld.overworld_map(parent);
273 if (parent_map == nullptr) {
274 return absl::InternalError("Overworld parent map missing after load");
275 }
276
277 int cols = 1;
278 int rows = 1;
279 std::string area_name = "small";
280 switch (parent_map->area_size()) {
282 cols = rows = 2;
283 area_name = "large";
284 break;
286 cols = 2;
287 area_name = "wide";
288 break;
290 rows = 2;
291 area_name = "tall";
292 break;
294 break;
295 }
296 if (area_name == "small" && parent_map->is_large_map()) {
297 cols = rows = 2;
298 area_name = "large";
299 }
300
301 constexpr int kScreen = 512;
302 const int width = cols * kScreen;
303 const int height = rows * kScreen;
304 std::vector<uint8_t> native(static_cast<size_t>(width) * height * 4, 0);
305
307 result.requested_screen = req.screen_id;
308 result.parent_screen = parent;
309 result.area_size = area_name;
310 result.graphics_preview = req.graphics_preview.contains(parent);
311
312 auto palette_rgb = [](const gfx::SnesPalette& palette, uint8_t idx,
313 uint8_t& r, uint8_t& g, uint8_t& b) {
314 r = g = b = 0;
315 if (idx < palette.size()) {
316 const auto rgb = palette[idx].rgb();
317 r = static_cast<uint8_t>(rgb.x);
318 g = static_cast<uint8_t>(rgb.y);
319 b = static_cast<uint8_t>(rgb.z);
320 }
321 };
322
323 for (int dy = 0; dy < rows; ++dy) {
324 for (int dx = 0; dx < cols; ++dx) {
325 const int screen = parent + dx + dy * 8;
326 // Subscreen overlay layer (sky, fog, lava, canopy, rain): drawn with
327 // this screen's graphics and palette, like the game's BG1.
328 std::vector<uint8_t> overlay_pixels;
329 bool overlay_is_background = false;
330 if (req.area_overlay) {
331 ASSIGN_OR_RETURN(auto layer,
332 overworld.BuildSubscreenOverlayLayer(screen));
333 if (layer.overlay_screen >= 0) {
334 overlay_pixels = std::move(layer.pixels);
335 overlay_is_background = layer.background;
336 result.subscreen_overlay = layer.overlay_screen;
337 }
338 }
339 RETURN_IF_ERROR(overworld.EnsureMapBuilt(screen));
340 const auto* map = overworld.overworld_map(screen);
341 if (map == nullptr) {
342 return absl::InternalError("Overworld child map missing");
343 }
344 const auto& pixels = map->bitmap_data();
345 const auto& palette = map->current_palette();
346 if (pixels.size() < static_cast<size_t>(kScreen) * kScreen) {
347 return absl::InternalError(
348 absl::StrFormat("Map 0x%02X has no bitmap after build", screen));
349 }
350 result.screens.push_back(screen);
351 for (int y = 0; y < kScreen; ++y) {
352 for (int x = 0; x < kScreen; ++x) {
353 const uint8_t idx = pixels[y * kScreen + x];
354 uint8_t r = 0, g = 0, b = 0;
355 palette_rgb(palette, idx, r, g, b);
356 if (!overlay_pixels.empty()) {
357 const uint8_t oidx = overlay_pixels[y * kScreen + x];
358 if (oidx != 0) {
359 uint8_t orr = 0, og = 0, ob = 0;
360 palette_rgb(palette, oidx, orr, og, ob);
361 if (overlay_is_background) {
362 // The layer only covers backdrop pixels.
363 r = orr;
364 g = og;
365 b = ob;
366 } else {
367 // Front overlays (fog, rain, canopy) blend at half strength.
368 r = static_cast<uint8_t>((r + orr) / 2);
369 g = static_cast<uint8_t>((g + og) / 2);
370 b = static_cast<uint8_t>((b + ob) / 2);
371 }
372 }
373 }
374 const size_t base = (static_cast<size_t>(dy * kScreen + y) * width +
375 dx * kScreen + x) *
376 4;
377 native[base + 0] = r;
378 native[base + 1] = g;
379 native[base + 2] = b;
380 native[base + 3] = 255;
381 }
382 }
383 }
384 }
385
386 const int out_w = static_cast<int>(width * req.scale);
387 const int out_h = static_cast<int>(height * req.scale);
388 std::vector<uint8_t> rgba(static_cast<size_t>(out_w) * out_h * 4, 255);
389 for (int oy = 0; oy < out_h; ++oy) {
390 const int sy = oy * height / out_h;
391 for (int ox = 0; ox < out_w; ++ox) {
392 const int sx = ox * width / out_w;
393 std::copy_n(native.begin() + (static_cast<size_t>(sy) * width + sx) * 4,
394 4, rgba.begin() + (static_cast<size_t>(oy) * out_w + ox) * 4);
395 }
396 }
397
398 HeadlessOverlayRenderer draw(rgba, out_w, out_h, req.scale);
399 const uint32_t flags = req.overlay_flags;
400 const int origin_x = (parent % 8) * kScreen;
401 const int origin_y = ((parent % 64) / 8) * kScreen;
402 auto owned = [&](int map_id) {
403 const auto* map = overworld.overworld_map(map_id);
404 return map != nullptr && map->parent() == parent;
405 };
406 auto in_area = [&](int lx, int ly) {
407 return lx >= 0 && ly >= 0 && lx < width && ly < height;
408 };
409
410 if (flags & OverworldOverlay::kGrid) {
411 for (int x = 0; x <= width; x += 16) {
412 const bool edge = x % kScreen == 0;
413 draw.DrawLine(static_cast<float>(x), 0, static_cast<float>(x),
414 static_cast<float>(height - 1), 255, 255, 255,
415 edge ? 180 : 40);
416 }
417 for (int y = 0; y <= height; y += 16) {
418 const bool edge = y % kScreen == 0;
419 draw.DrawLine(0, static_cast<float>(y), static_cast<float>(width - 1),
420 static_cast<float>(y), 255, 255, 255, edge ? 180 : 40);
421 }
422 }
423
424 auto mark = [&](const std::string& kind, int id, int lx, int ly,
425 const std::string& label, uint8_t r, uint8_t g, uint8_t b,
426 int phase = -1, int list_index = -1) {
427 draw.DrawFilledRect(static_cast<float>(lx), static_cast<float>(ly), 16, 16,
428 r, g, b, 90);
429 draw.DrawRect(static_cast<float>(lx), static_cast<float>(ly), 16, 16, r, g,
430 b, 255);
431 DrawLabel(draw, static_cast<float>(lx), static_cast<float>(ly + 17), label,
432 r, g, b);
433 result.markers.push_back({kind, id, phase, list_index, lx, ly});
434 };
435
436 if (flags & OverworldOverlay::kItems) {
437 for (const auto& item : overworld.all_items()) {
438 if (item.deleted || !owned(item.room_map_id_))
439 continue;
440 const int lx = item.x_ - origin_x;
441 const int ly = item.y_ - origin_y;
442 if (!in_area(lx, ly))
443 continue;
444 mark("item", item.id_, lx, ly, absl::StrFormat("I%02X", item.id_), 255,
445 230, 60);
446 }
447 }
448 if (flags & OverworldOverlay::kEntrances) {
449 for (const auto& entrance : overworld.entrances()) {
450 if (entrance.deleted || !owned(entrance.map_id_))
451 continue;
452 const int lx = entrance.x_ - origin_x;
453 const int ly = entrance.y_ - origin_y;
454 if (!in_area(lx, ly))
455 continue;
456 mark("entrance", entrance.entrance_id_, lx, ly,
457 absl::StrFormat("E%02X", entrance.entrance_id_), 255, 255, 255);
458 }
459 }
460 if (flags & OverworldOverlay::kHoles) {
461 for (const auto& hole : overworld.holes()) {
462 if (hole.deleted || !owned(hole.map_id_))
463 continue;
464 const int lx = hole.x_ - origin_x;
465 const int ly = hole.y_ - origin_y;
466 if (!in_area(lx, ly))
467 continue;
468 mark("hole", hole.entrance_id_, lx, ly,
469 absl::StrFormat("H%02X", hole.entrance_id_), 255, 150, 40);
470 }
471 }
472 if (flags & OverworldOverlay::kExits) {
473 for (const auto& exit : *overworld.exits()) {
474 if (exit.deleted_ || !owned(exit.map_id_))
475 continue;
476 const int lx = exit.x_ - origin_x;
477 const int ly = exit.y_ - origin_y;
478 if (!in_area(lx, ly))
479 continue;
480 mark("exit", exit.room_id_, lx, ly,
481 absl::StrFormat("X%02X", exit.room_id_), 200, 120, 255);
482 }
483 }
484 if (flags & OverworldOverlay::kSprites) {
485 for (int phase = 0; phase < 3; ++phase) {
486 if (req.phase >= 0 && req.phase != phase)
487 continue;
488 const auto color = SpritePhaseColor(phase);
489 int list_index = 0;
490 for (const auto& sprite : overworld.sprites(phase)) {
491 if (sprite.map_id() != parent)
492 continue;
493 const int index = list_index++;
494 if (sprite.deleted())
495 continue;
496 // Offset phases slightly so identical entries in two lists stay
497 // visible when every phase is drawn.
498 const int nudge = req.phase < 0 ? phase * 2 : 0;
499 const int lx = sprite.x() - origin_x + nudge;
500 const int ly = sprite.y() - origin_y + nudge;
501 if (!in_area(lx, ly))
502 continue;
503 const std::string label =
504 req.phase < 0 ? absl::StrFormat("%d:%02X", phase, sprite.id())
505 : absl::StrFormat("%02X", sprite.id());
506 mark("sprite", sprite.id(), lx, ly, label, color.r, color.g, color.b,
507 phase, index);
508 }
509 }
510 }
511
512 ASSIGN_OR_RETURN(result.png_data, EncodePng(rgba, out_w, out_h));
513 result.width = out_w;
514 result.height = out_h;
515 return result;
516}
517
518absl::StatusOr<RoomMetadata> RenderService::GetDungeonRoomMetadata(
519 int room_id) {
520 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
521 return absl::InvalidArgumentError(
522 absl::StrFormat("Invalid room_id 0x%02X", room_id));
523 }
524
525 std::lock_guard<std::mutex> lock(mu_);
526
529 room.LoadSprites();
530
531 RoomMetadata meta;
532 meta.room_id = room_id;
533 meta.blockset = room.blockset();
534 meta.spriteset = room.spriteset();
535 meta.palette = room.palette();
536 meta.layout_id = room.layout_id();
537 meta.effect = static_cast<int>(room.effect());
538 meta.collision = static_cast<int>(room.collision());
539 meta.tag1 = static_cast<int>(room.tag1());
540 meta.tag2 = static_cast<int>(room.tag2());
541 meta.message_id = room.message_id();
543 meta.object_count = static_cast<int>(room.GetTileObjects().size());
544 meta.sprite_count = static_cast<int>(room.GetSprites().size());
545 return meta;
546}
547
548// ---------------------------------------------------------------------------
549// Private helpers
550// ---------------------------------------------------------------------------
551
552absl::StatusOr<std::vector<uint8_t>> RenderService::BitmapToRgba(
553 const gfx::Bitmap& bitmap, int out_w, int out_h) {
554 SDL_Surface* surface = bitmap.surface();
555 if (!surface) {
556 return absl::InternalError("Bitmap has no SDL surface");
557 }
558
559 const int src_w = bitmap.width();
560 const int src_h = bitmap.height();
561
562 // Allocate RGBA output at target size.
563 std::vector<uint8_t> rgba(static_cast<size_t>(out_w) * out_h * 4, 0xFF);
564
565 // Nearest-neighbour scale: for each output pixel, sample the source.
566 for (int oy = 0; oy < out_h; ++oy) {
567 const int sy = oy * src_h / out_h;
568 for (int ox = 0; ox < out_w; ++ox) {
569 const int sx = ox * src_w / out_w;
570 if (sx >= src_w || sy >= src_h)
571 continue;
572 const SDL_Color color = bitmap.ReadColor(sx, sy);
573
574 const size_t base = static_cast<size_t>((oy * out_w + ox) * 4);
575 rgba[base + 0] = color.r;
576 rgba[base + 1] = color.g;
577 rgba[base + 2] = color.b;
578 rgba[base + 3] = color.a;
579 }
580 }
581
582 return rgba;
583}
584
585void RenderService::ApplyOverlays(std::vector<uint8_t>& rgba, int width,
586 int height, const zelda3::Room& room,
587 uint32_t flags, float scale) {
588 HeadlessOverlayRenderer draw(rgba, width, height, scale);
589
590 // Tile grid — draw first so other overlays paint on top.
591 if (flags & RenderOverlay::kGrid) {
592 for (int tx = 0; tx < 64; ++tx) {
593 draw.DrawLine(static_cast<float>(tx * 8), 0, static_cast<float>(tx * 8),
594 511, 60, 60, 60, 80);
595 }
596 for (int ty = 0; ty < 64; ++ty) {
597 draw.DrawLine(0, static_cast<float>(ty * 8), 511,
598 static_cast<float>(ty * 8), 60, 60, 60, 80);
599 }
600 }
601
602 // Custom collision overlay — one colored square per non-zero tile.
603 if ((flags & RenderOverlay::kCollision) || (flags & RenderOverlay::kTrack)) {
604 const auto& cc = room.custom_collision();
605 if (cc.has_data) {
606 for (int ty = 0; ty < 64; ++ty) {
607 for (int tx = 0; tx < 64; ++tx) {
608 const uint8_t val = cc.tiles[ty * 64 + tx];
609 if (val == 0)
610 continue;
611
612 // kCollision shows all non-zero tiles; kTrack shows only track tiles.
613 const bool is_track =
614 (val >= 0xB0 && val <= 0xBE) || (val >= 0xD0 && val <= 0xD3);
615 if (!(flags & RenderOverlay::kCollision) && !is_track)
616 continue;
617
618 const auto c = CollisionColor(val);
619 if (c.a == 0)
620 continue;
621 draw.DrawFilledRect(static_cast<float>(tx * 8),
622 static_cast<float>(ty * 8), 8.0f, 8.0f, c.r, c.g,
623 c.b, c.a);
624 }
625 }
626 }
627 }
628
629 // Vanilla tile types — pit cells (type 0x20) from the room's attribute
630 // maps: red where the upper map (COLMAPA, BG1) has a pit, orange where only
631 // the lower map (COLMAPB, BG2) does. See dungeon-tile-types.
632 if ((flags & RenderOverlay::kTileTypes) && rom_ != nullptr) {
633 const auto maps = zelda3::ComputeRoomCollisionMaps(
635 for (int ty = 0; ty < 64; ++ty) {
636 for (int tx = 0; tx < 64; ++tx) {
637 const bool upper = maps.upper(tx, ty) == 0x20;
638 const bool lower = maps.lower(tx, ty) == 0x20;
639 if (!upper && !lower)
640 continue;
641 draw.DrawFilledRect(static_cast<float>(tx * 8),
642 static_cast<float>(ty * 8), 8.0f, 8.0f, 255,
643 upper ? 0 : 140, 0, 150);
644 }
645 }
646 }
647
648 // Objects — draw outline of each tile object's bounding rect.
649 if (flags & RenderOverlay::kObjects) {
650 for (const auto& obj : room.GetTileObjects()) {
651 const float px = static_cast<float>(obj.x() * 8);
652 const float py = static_cast<float>(obj.y() * 8);
653 const float pw =
654 static_cast<float>(std::max(1, static_cast<int>(obj.width_)) * 8);
655 const float ph =
656 static_cast<float>(std::max(1, static_cast<int>(obj.height_)) * 8);
657 draw.DrawRect(px, py, pw, ph, 255, 200, 0, 200);
658 }
659 }
660
661 // Sprites — filled square at sprite tile position.
662 if (flags & RenderOverlay::kSprites) {
663 for (const auto& spr : room.GetSprites()) {
664 // Sprite nx/ny are in 16px units (2 tiles per unit).
665 const float px = static_cast<float>(spr.nx() * 16);
666 const float py = static_cast<float>(spr.ny() * 16);
667 draw.DrawFilledRect(px, py, 16.0f, 16.0f, 255, 60, 60, 120);
668 draw.DrawRect(px, py, 16.0f, 16.0f, 255, 60, 60, 230);
669 }
670 }
671
672 // Camera quadrant boundaries — two lines bisecting the room.
673 if (flags & RenderOverlay::kCameraQuads) {
674 draw.DrawLine(256, 0, 256, 511, 200, 200, 255, 120);
675 draw.DrawLine(0, 256, 511, 256, 200, 200, 255, 120);
676 }
677}
678
679#ifdef YAZE_CLI_HAS_PNG
680absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
681 const std::vector<uint8_t>& rgba, int width, int height) {
682 png_structp png =
683 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
684 if (!png)
685 return absl::InternalError("png_create_write_struct failed");
686
687 png_infop info = png_create_info_struct(png);
688 if (!info) {
689 png_destroy_write_struct(&png, nullptr);
690 return absl::InternalError("png_create_info_struct failed");
691 }
692
693 std::vector<uint8_t> output;
694 PngCtx ctx{&output};
695
696 if (setjmp(png_jmpbuf(png))) {
697 png_destroy_write_struct(&png, &info);
698 return absl::InternalError("PNG encoding error");
699 }
700
701 png_set_write_fn(png, &ctx, PngWrite, PngFlush);
702 png_set_IHDR(png, info, static_cast<uint32_t>(width),
703 static_cast<uint32_t>(height), 8, PNG_COLOR_TYPE_RGBA,
704 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
705 PNG_FILTER_TYPE_DEFAULT);
706 png_write_info(png, info);
707
708 std::vector<png_bytep> rows(static_cast<size_t>(height));
709 for (int row = 0; row < height; ++row) {
710 rows[static_cast<size_t>(row)] = const_cast<png_bytep>(
711 rgba.data() + static_cast<size_t>(row) * width * 4);
712 }
713 png_write_image(png, rows.data());
714 png_write_end(png, nullptr);
715 png_destroy_write_struct(&png, &info);
716
717 return output;
718}
719#else
720absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
721 const std::vector<uint8_t>& /*rgba*/, int /*width*/, int /*height*/) {
722 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
723}
724#endif
725
726} // namespace service
727} // namespace app
728} // namespace yaze
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:52
bool is_loaded() const
Definition rom.h:199
void DrawRect(float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
void DrawLine(float x0, float y0, float x1, float y1, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
void DrawFilledRect(float x, float y, float w, float h, uint8_t r, uint8_t g, uint8_t b, uint8_t a)
absl::StatusOr< std::vector< uint8_t > > EncodePng(const std::vector< uint8_t > &rgba, int width, int height)
RenderService(Rom *rom, zelda3::GameData *game_data)
absl::StatusOr< RoomMetadata > GetDungeonRoomMetadata(int room_id)
absl::StatusOr< OverworldRenderResult > RenderOverworldArea(const OverworldRenderRequest &req)
void ApplyOverlays(std::vector< uint8_t > &rgba, int width, int height, const zelda3::Room &room, uint32_t flags, float scale)
absl::StatusOr< std::vector< uint8_t > > BitmapToRgba(const gfx::Bitmap &bitmap, int width, int height)
absl::StatusOr< RenderResult > RenderDungeonRoom(const RenderRequest &req)
Represents a bitmap image optimized for SNES ROM hacking.
Definition bitmap.h:70
int height() const
Definition bitmap.h:399
int width() const
Definition bitmap.h:398
SDL_Color ReadColor(int x, int y) const
Definition bitmap.cc:414
SDL_Surface * surface() const
Definition bitmap.h:409
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
absl::Status Load(Rom *rom)
Load all overworld data from ROM.
Definition overworld.cc:158
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:808
auto sprites(int state) const
Definition overworld.h:787
auto all_items() const
Definition overworld.h:868
absl::StatusOr< SubscreenOverlayLayer > BuildSubscreenOverlayLayer(int map_index)
Build the subscreen overlay layer for a screen (builds the screen if needed). The overlay comes from ...
auto overworld_map(int i) const
Definition overworld.h:770
void SetGraphicsPreview(OverworldGraphicsPreview preview)
absl::Status EnsureMapBuilt(int map_index)
Build a map on-demand if it hasn't been built yet.
const std::vector< OverworldEntrance > & entrances() const
Definition overworld.h:803
RoomLayerManager - Manages layer visibility and compositing.
void ApplyGameLayerRegisters(const RoomLayerRegisters &registers)
void ApplyLayerMerging(const LayerMergeType &merge_type)
void ApplyRoomEffect(EffectKey effect)
const CustomCollisionMap & custom_collision() const
Definition room.h:559
uint16_t message_id() const
Definition room.h:1029
RoomLayerRegisters GameLayerRegisters(uint16_t room_flags=0) const
Definition room.cc:943
uint8_t blockset() const
Definition room.h:1015
void SetGraphicsSheetOverrides(std::map< uint16_t, std::vector< uint8_t > > overrides)
Definition room.h:1099
TagKey tag2() const
Definition room.h:988
uint8_t palette() const
Definition room.h:1019
void RenderRoomGraphics()
Definition room.cc:1254
TagKey tag1() const
Definition room.h:987
CollisionKey collision() const
Definition room.h:989
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:953
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
Definition room.cc:1230
uint8_t spriteset() const
Definition room.h:1018
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:316
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:445
const LayerMergeType & layer_merging() const
Definition room.h:990
EffectKey effect() const
Definition room.h:986
void LoadSprites()
Definition room.cc:2887
uint8_t layout_id() const
Definition room.h:1026
bool has_custom_collision() const
Definition room.h:563
void SetGameData(GameData *data)
Definition room.h:1088
#define ASSIGN_OR_RETURN(type_variable_name, expression)
Definition macro.h:62
void DrawLabel(HeadlessOverlayRenderer &draw, float x, float y, const std::string &text, uint8_t r, uint8_t g, uint8_t b)
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
constexpr int kNumOverworldMaps
Definition common.h:85
RoomCollisionMaps ComputeRoomCollisionMaps(const Rom &rom, const RoomCollisionInput &input, const UnderworldRoomLoadState &state)
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:655
constexpr int kNumberOfRooms
RoomCollisionInput MakeRoomCollisionInput(const Room &room, const RoomTilemaps &tilemaps)
SDL2/SDL3 compatibility layer.
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
zelda3::OverworldGraphicsPreview graphics_preview
std::vector< OverworldRenderMarker > markers
std::map< uint16_t, std::vector< uint8_t > > sheet_overrides
std::vector< uint8_t > png_data
std::array< uint8_t, 64 *64 > tiles