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
264 const auto* requested = overworld.overworld_map(req.screen_id);
265 if (requested == nullptr) {
266 return absl::InternalError("Overworld map missing after load");
267 }
268 const int parent = requested->parent();
269 const auto* parent_map = overworld.overworld_map(parent);
270 if (parent_map == nullptr) {
271 return absl::InternalError("Overworld parent map missing after load");
272 }
273
274 int cols = 1;
275 int rows = 1;
276 std::string area_name = "small";
277 switch (parent_map->area_size()) {
279 cols = rows = 2;
280 area_name = "large";
281 break;
283 cols = 2;
284 area_name = "wide";
285 break;
287 rows = 2;
288 area_name = "tall";
289 break;
291 break;
292 }
293 if (area_name == "small" && parent_map->is_large_map()) {
294 cols = rows = 2;
295 area_name = "large";
296 }
297
298 constexpr int kScreen = 512;
299 const int width = cols * kScreen;
300 const int height = rows * kScreen;
301 std::vector<uint8_t> native(static_cast<size_t>(width) * height * 4, 0);
302
304 result.requested_screen = req.screen_id;
305 result.parent_screen = parent;
306 result.area_size = area_name;
307
308 auto palette_rgb = [](const gfx::SnesPalette& palette, uint8_t idx,
309 uint8_t& r, uint8_t& g, uint8_t& b) {
310 r = g = b = 0;
311 if (idx < palette.size()) {
312 const auto rgb = palette[idx].rgb();
313 r = static_cast<uint8_t>(rgb.x);
314 g = static_cast<uint8_t>(rgb.y);
315 b = static_cast<uint8_t>(rgb.z);
316 }
317 };
318
319 for (int dy = 0; dy < rows; ++dy) {
320 for (int dx = 0; dx < cols; ++dx) {
321 const int screen = parent + dx + dy * 8;
322 // Subscreen overlay layer (sky, fog, lava, canopy, rain): drawn with
323 // this screen's graphics and palette, like the game's BG1.
324 std::vector<uint8_t> overlay_pixels;
325 bool overlay_is_background = false;
326 if (req.area_overlay) {
327 ASSIGN_OR_RETURN(auto layer,
328 overworld.BuildSubscreenOverlayLayer(screen));
329 if (layer.overlay_screen >= 0) {
330 overlay_pixels = std::move(layer.pixels);
331 overlay_is_background = layer.background;
332 result.subscreen_overlay = layer.overlay_screen;
333 }
334 }
335 RETURN_IF_ERROR(overworld.EnsureMapBuilt(screen));
336 const auto* map = overworld.overworld_map(screen);
337 if (map == nullptr) {
338 return absl::InternalError("Overworld child map missing");
339 }
340 const auto& pixels = map->bitmap_data();
341 const auto& palette = map->current_palette();
342 if (pixels.size() < static_cast<size_t>(kScreen) * kScreen) {
343 return absl::InternalError(
344 absl::StrFormat("Map 0x%02X has no bitmap after build", screen));
345 }
346 result.screens.push_back(screen);
347 for (int y = 0; y < kScreen; ++y) {
348 for (int x = 0; x < kScreen; ++x) {
349 const uint8_t idx = pixels[y * kScreen + x];
350 uint8_t r = 0, g = 0, b = 0;
351 palette_rgb(palette, idx, r, g, b);
352 if (!overlay_pixels.empty()) {
353 const uint8_t oidx = overlay_pixels[y * kScreen + x];
354 if (oidx != 0) {
355 uint8_t orr = 0, og = 0, ob = 0;
356 palette_rgb(palette, oidx, orr, og, ob);
357 if (overlay_is_background) {
358 // The layer only covers backdrop pixels.
359 r = orr;
360 g = og;
361 b = ob;
362 } else {
363 // Front overlays (fog, rain, canopy) blend at half strength.
364 r = static_cast<uint8_t>((r + orr) / 2);
365 g = static_cast<uint8_t>((g + og) / 2);
366 b = static_cast<uint8_t>((b + ob) / 2);
367 }
368 }
369 }
370 const size_t base = (static_cast<size_t>(dy * kScreen + y) * width +
371 dx * kScreen + x) *
372 4;
373 native[base + 0] = r;
374 native[base + 1] = g;
375 native[base + 2] = b;
376 native[base + 3] = 255;
377 }
378 }
379 }
380 }
381
382 const int out_w = static_cast<int>(width * req.scale);
383 const int out_h = static_cast<int>(height * req.scale);
384 std::vector<uint8_t> rgba(static_cast<size_t>(out_w) * out_h * 4, 255);
385 for (int oy = 0; oy < out_h; ++oy) {
386 const int sy = oy * height / out_h;
387 for (int ox = 0; ox < out_w; ++ox) {
388 const int sx = ox * width / out_w;
389 std::copy_n(native.begin() + (static_cast<size_t>(sy) * width + sx) * 4,
390 4, rgba.begin() + (static_cast<size_t>(oy) * out_w + ox) * 4);
391 }
392 }
393
394 HeadlessOverlayRenderer draw(rgba, out_w, out_h, req.scale);
395 const uint32_t flags = req.overlay_flags;
396 const int origin_x = (parent % 8) * kScreen;
397 const int origin_y = ((parent % 64) / 8) * kScreen;
398 auto owned = [&](int map_id) {
399 const auto* map = overworld.overworld_map(map_id);
400 return map != nullptr && map->parent() == parent;
401 };
402 auto in_area = [&](int lx, int ly) {
403 return lx >= 0 && ly >= 0 && lx < width && ly < height;
404 };
405
406 if (flags & OverworldOverlay::kGrid) {
407 for (int x = 0; x <= width; x += 16) {
408 const bool edge = x % kScreen == 0;
409 draw.DrawLine(static_cast<float>(x), 0, static_cast<float>(x),
410 static_cast<float>(height - 1), 255, 255, 255,
411 edge ? 180 : 40);
412 }
413 for (int y = 0; y <= height; y += 16) {
414 const bool edge = y % kScreen == 0;
415 draw.DrawLine(0, static_cast<float>(y), static_cast<float>(width - 1),
416 static_cast<float>(y), 255, 255, 255, edge ? 180 : 40);
417 }
418 }
419
420 auto mark = [&](const std::string& kind, int id, int lx, int ly,
421 const std::string& label, uint8_t r, uint8_t g, uint8_t b,
422 int phase = -1, int list_index = -1) {
423 draw.DrawFilledRect(static_cast<float>(lx), static_cast<float>(ly), 16, 16,
424 r, g, b, 90);
425 draw.DrawRect(static_cast<float>(lx), static_cast<float>(ly), 16, 16, r, g,
426 b, 255);
427 DrawLabel(draw, static_cast<float>(lx), static_cast<float>(ly + 17), label,
428 r, g, b);
429 result.markers.push_back({kind, id, phase, list_index, lx, ly});
430 };
431
432 if (flags & OverworldOverlay::kItems) {
433 for (const auto& item : overworld.all_items()) {
434 if (item.deleted || !owned(item.room_map_id_))
435 continue;
436 const int lx = item.x_ - origin_x;
437 const int ly = item.y_ - origin_y;
438 if (!in_area(lx, ly))
439 continue;
440 mark("item", item.id_, lx, ly, absl::StrFormat("I%02X", item.id_), 255,
441 230, 60);
442 }
443 }
444 if (flags & OverworldOverlay::kEntrances) {
445 for (const auto& entrance : overworld.entrances()) {
446 if (entrance.deleted || !owned(entrance.map_id_))
447 continue;
448 const int lx = entrance.x_ - origin_x;
449 const int ly = entrance.y_ - origin_y;
450 if (!in_area(lx, ly))
451 continue;
452 mark("entrance", entrance.entrance_id_, lx, ly,
453 absl::StrFormat("E%02X", entrance.entrance_id_), 255, 255, 255);
454 }
455 }
456 if (flags & OverworldOverlay::kHoles) {
457 for (const auto& hole : overworld.holes()) {
458 if (hole.deleted || !owned(hole.map_id_))
459 continue;
460 const int lx = hole.x_ - origin_x;
461 const int ly = hole.y_ - origin_y;
462 if (!in_area(lx, ly))
463 continue;
464 mark("hole", hole.entrance_id_, lx, ly,
465 absl::StrFormat("H%02X", hole.entrance_id_), 255, 150, 40);
466 }
467 }
468 if (flags & OverworldOverlay::kExits) {
469 for (const auto& exit : *overworld.exits()) {
470 if (exit.deleted_ || !owned(exit.map_id_))
471 continue;
472 const int lx = exit.x_ - origin_x;
473 const int ly = exit.y_ - origin_y;
474 if (!in_area(lx, ly))
475 continue;
476 mark("exit", exit.room_id_, lx, ly,
477 absl::StrFormat("X%02X", exit.room_id_), 200, 120, 255);
478 }
479 }
480 if (flags & OverworldOverlay::kSprites) {
481 for (int phase = 0; phase < 3; ++phase) {
482 if (req.phase >= 0 && req.phase != phase)
483 continue;
484 const auto color = SpritePhaseColor(phase);
485 int list_index = 0;
486 for (const auto& sprite : overworld.sprites(phase)) {
487 if (sprite.map_id() != parent)
488 continue;
489 const int index = list_index++;
490 if (sprite.deleted())
491 continue;
492 // Offset phases slightly so identical entries in two lists stay
493 // visible when every phase is drawn.
494 const int nudge = req.phase < 0 ? phase * 2 : 0;
495 const int lx = sprite.x() - origin_x + nudge;
496 const int ly = sprite.y() - origin_y + nudge;
497 if (!in_area(lx, ly))
498 continue;
499 const std::string label =
500 req.phase < 0 ? absl::StrFormat("%d:%02X", phase, sprite.id())
501 : absl::StrFormat("%02X", sprite.id());
502 mark("sprite", sprite.id(), lx, ly, label, color.r, color.g, color.b,
503 phase, index);
504 }
505 }
506 }
507
508 ASSIGN_OR_RETURN(result.png_data, EncodePng(rgba, out_w, out_h));
509 result.width = out_w;
510 result.height = out_h;
511 return result;
512}
513
514absl::StatusOr<RoomMetadata> RenderService::GetDungeonRoomMetadata(
515 int room_id) {
516 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
517 return absl::InvalidArgumentError(
518 absl::StrFormat("Invalid room_id 0x%02X", room_id));
519 }
520
521 std::lock_guard<std::mutex> lock(mu_);
522
525 room.LoadSprites();
526
527 RoomMetadata meta;
528 meta.room_id = room_id;
529 meta.blockset = room.blockset();
530 meta.spriteset = room.spriteset();
531 meta.palette = room.palette();
532 meta.layout_id = room.layout_id();
533 meta.effect = static_cast<int>(room.effect());
534 meta.collision = static_cast<int>(room.collision());
535 meta.tag1 = static_cast<int>(room.tag1());
536 meta.tag2 = static_cast<int>(room.tag2());
537 meta.message_id = room.message_id();
539 meta.object_count = static_cast<int>(room.GetTileObjects().size());
540 meta.sprite_count = static_cast<int>(room.GetSprites().size());
541 return meta;
542}
543
544// ---------------------------------------------------------------------------
545// Private helpers
546// ---------------------------------------------------------------------------
547
548absl::StatusOr<std::vector<uint8_t>> RenderService::BitmapToRgba(
549 const gfx::Bitmap& bitmap, int out_w, int out_h) {
550 SDL_Surface* surface = bitmap.surface();
551 if (!surface) {
552 return absl::InternalError("Bitmap has no SDL surface");
553 }
554
555 const int src_w = bitmap.width();
556 const int src_h = bitmap.height();
557
558 // Allocate RGBA output at target size.
559 std::vector<uint8_t> rgba(static_cast<size_t>(out_w) * out_h * 4, 0xFF);
560
561 // Nearest-neighbour scale: for each output pixel, sample the source.
562 for (int oy = 0; oy < out_h; ++oy) {
563 const int sy = oy * src_h / out_h;
564 for (int ox = 0; ox < out_w; ++ox) {
565 const int sx = ox * src_w / out_w;
566 if (sx >= src_w || sy >= src_h)
567 continue;
568 const SDL_Color color = bitmap.ReadColor(sx, sy);
569
570 const size_t base = static_cast<size_t>((oy * out_w + ox) * 4);
571 rgba[base + 0] = color.r;
572 rgba[base + 1] = color.g;
573 rgba[base + 2] = color.b;
574 rgba[base + 3] = color.a;
575 }
576 }
577
578 return rgba;
579}
580
581void RenderService::ApplyOverlays(std::vector<uint8_t>& rgba, int width,
582 int height, const zelda3::Room& room,
583 uint32_t flags, float scale) {
584 HeadlessOverlayRenderer draw(rgba, width, height, scale);
585
586 // Tile grid — draw first so other overlays paint on top.
587 if (flags & RenderOverlay::kGrid) {
588 for (int tx = 0; tx < 64; ++tx) {
589 draw.DrawLine(static_cast<float>(tx * 8), 0, static_cast<float>(tx * 8),
590 511, 60, 60, 60, 80);
591 }
592 for (int ty = 0; ty < 64; ++ty) {
593 draw.DrawLine(0, static_cast<float>(ty * 8), 511,
594 static_cast<float>(ty * 8), 60, 60, 60, 80);
595 }
596 }
597
598 // Custom collision overlay — one colored square per non-zero tile.
599 if ((flags & RenderOverlay::kCollision) || (flags & RenderOverlay::kTrack)) {
600 const auto& cc = room.custom_collision();
601 if (cc.has_data) {
602 for (int ty = 0; ty < 64; ++ty) {
603 for (int tx = 0; tx < 64; ++tx) {
604 const uint8_t val = cc.tiles[ty * 64 + tx];
605 if (val == 0)
606 continue;
607
608 // kCollision shows all non-zero tiles; kTrack shows only track tiles.
609 const bool is_track =
610 (val >= 0xB0 && val <= 0xBE) || (val >= 0xD0 && val <= 0xD3);
611 if (!(flags & RenderOverlay::kCollision) && !is_track)
612 continue;
613
614 const auto c = CollisionColor(val);
615 if (c.a == 0)
616 continue;
617 draw.DrawFilledRect(static_cast<float>(tx * 8),
618 static_cast<float>(ty * 8), 8.0f, 8.0f, c.r, c.g,
619 c.b, c.a);
620 }
621 }
622 }
623 }
624
625 // Vanilla tile types — pit cells (type 0x20) from the room's attribute
626 // maps: red where the upper map (COLMAPA, BG1) has a pit, orange where only
627 // the lower map (COLMAPB, BG2) does. See dungeon-tile-types.
628 if ((flags & RenderOverlay::kTileTypes) && rom_ != nullptr) {
629 const auto maps = zelda3::ComputeRoomCollisionMaps(
631 for (int ty = 0; ty < 64; ++ty) {
632 for (int tx = 0; tx < 64; ++tx) {
633 const bool upper = maps.upper(tx, ty) == 0x20;
634 const bool lower = maps.lower(tx, ty) == 0x20;
635 if (!upper && !lower)
636 continue;
637 draw.DrawFilledRect(static_cast<float>(tx * 8),
638 static_cast<float>(ty * 8), 8.0f, 8.0f, 255,
639 upper ? 0 : 140, 0, 150);
640 }
641 }
642 }
643
644 // Objects — draw outline of each tile object's bounding rect.
645 if (flags & RenderOverlay::kObjects) {
646 for (const auto& obj : room.GetTileObjects()) {
647 const float px = static_cast<float>(obj.x() * 8);
648 const float py = static_cast<float>(obj.y() * 8);
649 const float pw =
650 static_cast<float>(std::max(1, static_cast<int>(obj.width_)) * 8);
651 const float ph =
652 static_cast<float>(std::max(1, static_cast<int>(obj.height_)) * 8);
653 draw.DrawRect(px, py, pw, ph, 255, 200, 0, 200);
654 }
655 }
656
657 // Sprites — filled square at sprite tile position.
658 if (flags & RenderOverlay::kSprites) {
659 for (const auto& spr : room.GetSprites()) {
660 // Sprite nx/ny are in 16px units (2 tiles per unit).
661 const float px = static_cast<float>(spr.nx() * 16);
662 const float py = static_cast<float>(spr.ny() * 16);
663 draw.DrawFilledRect(px, py, 16.0f, 16.0f, 255, 60, 60, 120);
664 draw.DrawRect(px, py, 16.0f, 16.0f, 255, 60, 60, 230);
665 }
666 }
667
668 // Camera quadrant boundaries — two lines bisecting the room.
669 if (flags & RenderOverlay::kCameraQuads) {
670 draw.DrawLine(256, 0, 256, 511, 200, 200, 255, 120);
671 draw.DrawLine(0, 256, 511, 256, 200, 200, 255, 120);
672 }
673}
674
675#ifdef YAZE_CLI_HAS_PNG
676absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
677 const std::vector<uint8_t>& rgba, int width, int height) {
678 png_structp png =
679 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
680 if (!png)
681 return absl::InternalError("png_create_write_struct failed");
682
683 png_infop info = png_create_info_struct(png);
684 if (!info) {
685 png_destroy_write_struct(&png, nullptr);
686 return absl::InternalError("png_create_info_struct failed");
687 }
688
689 std::vector<uint8_t> output;
690 PngCtx ctx{&output};
691
692 if (setjmp(png_jmpbuf(png))) {
693 png_destroy_write_struct(&png, &info);
694 return absl::InternalError("PNG encoding error");
695 }
696
697 png_set_write_fn(png, &ctx, PngWrite, PngFlush);
698 png_set_IHDR(png, info, static_cast<uint32_t>(width),
699 static_cast<uint32_t>(height), 8, PNG_COLOR_TYPE_RGBA,
700 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
701 PNG_FILTER_TYPE_DEFAULT);
702 png_write_info(png, info);
703
704 std::vector<png_bytep> rows(static_cast<size_t>(height));
705 for (int row = 0; row < height; ++row) {
706 rows[static_cast<size_t>(row)] = const_cast<png_bytep>(
707 rgba.data() + static_cast<size_t>(row) * width * 4);
708 }
709 png_write_image(png, rows.data());
710 png_write_end(png, nullptr);
711 png_destroy_write_struct(&png, &info);
712
713 return output;
714}
715#else
716absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
717 const std::vector<uint8_t>& /*rgba*/, int /*width*/, int /*height*/) {
718 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
719}
720#endif
721
722} // namespace service
723} // namespace app
724} // 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:49
bool is_loaded() const
Definition rom.h:187
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:136
const std::vector< OverworldEntrance > & holes() const
Definition overworld.h:775
auto sprites(int state) const
Definition overworld.h:754
auto all_items() const
Definition overworld.h:833
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:737
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:770
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:2886
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
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