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 "zelda3/dungeon/room.h"
17
18#include <SDL.h>
19#ifdef YAZE_CLI_HAS_PNG
20#include <png.h>
21#endif
22
23namespace yaze {
24namespace app {
25namespace service {
26
27namespace {
28
29absl::Status ValidateRenderScale(float scale) {
30 if (!std::isfinite(scale) || scale < 0.25f || scale > 8.0f) {
31 return absl::InvalidArgumentError(
32 "Render scale must be finite and between 0.25 and 8.0");
33 }
34 return absl::OkStatus();
35}
36
37#ifdef YAZE_CLI_HAS_PNG
38// PNG write helpers (mirrored from visual_diff_engine.cc).
39struct PngCtx {
40 std::vector<uint8_t>* buf;
41};
42
43void PngWrite(png_structp png, png_bytep data, png_size_t len) {
44 auto* ctx = static_cast<PngCtx*>(png_get_io_ptr(png));
45 ctx->buf->insert(ctx->buf->end(), data, data + len);
46}
47
48void PngFlush(png_structp /*png*/) {}
49#endif
50
51// Returns the overlay RGBA color for a custom-collision tile value.
52// Returns alpha=0 for tiles we don't want to highlight.
53struct TileColor {
54 uint8_t r, g, b, a;
55};
56
58 if (tile == 0)
59 return {0, 0, 0, 0};
60 if (tile == 0xB0 || tile == 0xB1)
61 return {0, 210, 170, 150}; // straight — teal
62 if (tile >= 0xB2 && tile <= 0xB5)
63 return {0, 190, 255, 150}; // corner — cyan
64 if (tile == 0xB6)
65 return {255, 215, 0, 170}; // intersection — gold
66 if (tile >= 0xB7 && tile <= 0xBA)
67 return {255, 80, 0, 200}; // stop — orange-red
68 if (tile >= 0xBB && tile <= 0xBE)
69 return {80, 150, 255, 150}; // T-junction — blue
70 if (tile >= 0xD0 && tile <= 0xD3)
71 return {210, 0, 255, 170}; // switch corner — magenta
72 return {140, 140, 140, 120}; // other non-zero — grey
73}
74
75} // namespace
76
77absl::StatusOr<float> ParseRenderScale(absl::string_view value) {
78 float scale = 0;
79 if (!absl::SimpleAtof(value, &scale)) {
80 return absl::InvalidArgumentError(
81 absl::StrFormat("Invalid scale value: %s", value));
82 }
83 if (const auto status = ValidateRenderScale(scale); !status.ok()) {
84 return status;
85 }
86 return scale;
87}
88
90 : rom_(rom), game_data_(game_data) {}
91
92absl::StatusOr<RenderResult> RenderService::RenderDungeonRoom(
93 const RenderRequest& req) {
94 if (const auto status = ValidateRenderScale(req.scale); !status.ok()) {
95 return status;
96 }
97 if (!rom_ || !rom_->is_loaded()) {
98 return absl::FailedPreconditionError("ROM not loaded");
99 }
100 if (!game_data_) {
101 return absl::FailedPreconditionError("GameData not available");
102 }
103 if (req.room_id < 0 || req.room_id >= zelda3::kNumberOfRooms) {
104 return absl::InvalidArgumentError(
105 absl::StrFormat("Invalid room_id 0x%02X", req.room_id));
106 }
107
108#ifndef YAZE_CLI_HAS_PNG
109 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
110#endif
111
112 std::lock_guard<std::mutex> lock(mu_);
113
114 // Load room data from ROM (header, objects, pots, torches, blocks, pits).
117
118 // Load sprites (requires ROM, game_data not needed here).
119 room.LoadSprites();
120
121 // Load graphics sheets and render tiles to BackgroundBuffers (CPU only).
122 room.LoadRoomGraphics();
123 room.RenderRoomGraphics();
124
125 // Composite all layers to a single Bitmap (CPU, SDL surface with palette).
126 zelda3::RoomLayerManager layer_mgr;
127 layer_mgr.ApplyLayerMerging(room.layer_merging());
128 layer_mgr.ApplyRoomEffect(room.effect());
129 auto& composite = room.GetCompositeBitmap(layer_mgr);
130
131 if (!composite.is_active() || composite.width() <= 0 ||
132 composite.height() <= 0) {
133 return absl::InternalError("Composite bitmap is empty after render");
134 }
135
136 const int out_w = static_cast<int>(composite.width() * req.scale);
137 const int out_h = static_cast<int>(composite.height() * req.scale);
138
139 // Convert indexed+palette bitmap to RGBA bytes.
140 auto rgba_or = BitmapToRgba(composite, out_w, out_h);
141 if (!rgba_or.ok())
142 return rgba_or.status();
143 auto rgba = std::move(rgba_or).value();
144
145 // Paint overlays directly into the RGBA buffer.
147 ApplyOverlays(rgba, out_w, out_h, room, req.overlay_flags, req.scale);
148 }
149
150 // Encode to PNG.
151 auto png_or = EncodePng(rgba, out_w, out_h);
152 if (!png_or.ok())
153 return png_or.status();
154
155 RenderResult result;
156 result.png_data = std::move(png_or).value();
157 result.width = out_w;
158 result.height = out_h;
159 return result;
160}
161
162absl::StatusOr<RoomMetadata> RenderService::GetDungeonRoomMetadata(
163 int room_id) {
164 if (room_id < 0 || room_id >= zelda3::kNumberOfRooms) {
165 return absl::InvalidArgumentError(
166 absl::StrFormat("Invalid room_id 0x%02X", room_id));
167 }
168
169 std::lock_guard<std::mutex> lock(mu_);
170
173 room.LoadSprites();
174
175 RoomMetadata meta;
176 meta.room_id = room_id;
177 meta.blockset = room.blockset();
178 meta.spriteset = room.spriteset();
179 meta.palette = room.palette();
180 meta.layout_id = room.layout_id();
181 meta.effect = static_cast<int>(room.effect());
182 meta.collision = static_cast<int>(room.collision());
183 meta.tag1 = static_cast<int>(room.tag1());
184 meta.tag2 = static_cast<int>(room.tag2());
185 meta.message_id = room.message_id();
187 meta.object_count = static_cast<int>(room.GetTileObjects().size());
188 meta.sprite_count = static_cast<int>(room.GetSprites().size());
189 return meta;
190}
191
192// ---------------------------------------------------------------------------
193// Private helpers
194// ---------------------------------------------------------------------------
195
196absl::StatusOr<std::vector<uint8_t>> RenderService::BitmapToRgba(
197 const gfx::Bitmap& bitmap, int out_w, int out_h) {
198 SDL_Surface* surface = bitmap.surface();
199 if (!surface) {
200 return absl::InternalError("Bitmap has no SDL surface");
201 }
202
203 const int src_w = bitmap.width();
204 const int src_h = bitmap.height();
205
206 // Get the palette from the surface.
207 SDL_Palette* pal = platform::GetSurfacePalette(surface);
208
209 const uint8_t* indexed = bitmap.data();
210 if (!indexed) {
211 return absl::InternalError("Bitmap has no pixel data");
212 }
213
214 // Allocate RGBA output at target size.
215 std::vector<uint8_t> rgba(static_cast<size_t>(out_w) * out_h * 4, 0xFF);
216
217 // Nearest-neighbour scale: for each output pixel, sample the source.
218 for (int oy = 0; oy < out_h; ++oy) {
219 const int sy = oy * src_h / out_h;
220 for (int ox = 0; ox < out_w; ++ox) {
221 const int sx = ox * src_w / out_w;
222 if (sx >= src_w || sy >= src_h)
223 continue;
224 const uint8_t idx = indexed[sy * src_w + sx];
225
226 uint8_t r = 0, g = 0, b = 0;
227 if (pal && static_cast<int>(idx) < pal->ncolors) {
228 r = pal->colors[idx].r;
229 g = pal->colors[idx].g;
230 b = pal->colors[idx].b;
231 }
232
233 const size_t base = static_cast<size_t>((oy * out_w + ox) * 4);
234 rgba[base + 0] = r;
235 rgba[base + 1] = g;
236 rgba[base + 2] = b;
237 rgba[base + 3] = 255;
238 }
239 }
240
241 return rgba;
242}
243
244void RenderService::ApplyOverlays(std::vector<uint8_t>& rgba, int width,
245 int height, const zelda3::Room& room,
246 uint32_t flags, float scale) {
247 HeadlessOverlayRenderer draw(rgba, width, height, scale);
248
249 // Tile grid — draw first so other overlays paint on top.
250 if (flags & RenderOverlay::kGrid) {
251 for (int tx = 0; tx < 64; ++tx) {
252 draw.DrawLine(static_cast<float>(tx * 8), 0, static_cast<float>(tx * 8),
253 511, 60, 60, 60, 80);
254 }
255 for (int ty = 0; ty < 64; ++ty) {
256 draw.DrawLine(0, static_cast<float>(ty * 8), 511,
257 static_cast<float>(ty * 8), 60, 60, 60, 80);
258 }
259 }
260
261 // Custom collision overlay — one colored square per non-zero tile.
262 if ((flags & RenderOverlay::kCollision) || (flags & RenderOverlay::kTrack)) {
263 const auto& cc = room.custom_collision();
264 if (cc.has_data) {
265 for (int ty = 0; ty < 64; ++ty) {
266 for (int tx = 0; tx < 64; ++tx) {
267 const uint8_t val = cc.tiles[ty * 64 + tx];
268 if (val == 0)
269 continue;
270
271 // kCollision shows all non-zero tiles; kTrack shows only track tiles.
272 const bool is_track =
273 (val >= 0xB0 && val <= 0xBE) || (val >= 0xD0 && val <= 0xD3);
274 if (!(flags & RenderOverlay::kCollision) && !is_track)
275 continue;
276
277 const auto c = CollisionColor(val);
278 if (c.a == 0)
279 continue;
280 draw.DrawFilledRect(static_cast<float>(tx * 8),
281 static_cast<float>(ty * 8), 8.0f, 8.0f, c.r, c.g,
282 c.b, c.a);
283 }
284 }
285 }
286 }
287
288 // Objects — draw outline of each tile object's bounding rect.
289 if (flags & RenderOverlay::kObjects) {
290 for (const auto& obj : room.GetTileObjects()) {
291 const float px = static_cast<float>(obj.x() * 8);
292 const float py = static_cast<float>(obj.y() * 8);
293 const float pw =
294 static_cast<float>(std::max(1, static_cast<int>(obj.width_)) * 8);
295 const float ph =
296 static_cast<float>(std::max(1, static_cast<int>(obj.height_)) * 8);
297 draw.DrawRect(px, py, pw, ph, 255, 200, 0, 200);
298 }
299 }
300
301 // Sprites — filled square at sprite tile position.
302 if (flags & RenderOverlay::kSprites) {
303 for (const auto& spr : room.GetSprites()) {
304 // Sprite nx/ny are in 16px units (2 tiles per unit).
305 const float px = static_cast<float>(spr.nx() * 16);
306 const float py = static_cast<float>(spr.ny() * 16);
307 draw.DrawFilledRect(px, py, 16.0f, 16.0f, 255, 60, 60, 120);
308 draw.DrawRect(px, py, 16.0f, 16.0f, 255, 60, 60, 230);
309 }
310 }
311
312 // Camera quadrant boundaries — two lines bisecting the room.
313 if (flags & RenderOverlay::kCameraQuads) {
314 draw.DrawLine(256, 0, 256, 511, 200, 200, 255, 120);
315 draw.DrawLine(0, 256, 511, 256, 200, 200, 255, 120);
316 }
317}
318
319#ifdef YAZE_CLI_HAS_PNG
320absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
321 const std::vector<uint8_t>& rgba, int width, int height) {
322 png_structp png =
323 png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
324 if (!png)
325 return absl::InternalError("png_create_write_struct failed");
326
327 png_infop info = png_create_info_struct(png);
328 if (!info) {
329 png_destroy_write_struct(&png, nullptr);
330 return absl::InternalError("png_create_info_struct failed");
331 }
332
333 std::vector<uint8_t> output;
334 PngCtx ctx{&output};
335
336 if (setjmp(png_jmpbuf(png))) {
337 png_destroy_write_struct(&png, &info);
338 return absl::InternalError("PNG encoding error");
339 }
340
341 png_set_write_fn(png, &ctx, PngWrite, PngFlush);
342 png_set_IHDR(png, info, static_cast<uint32_t>(width),
343 static_cast<uint32_t>(height), 8, PNG_COLOR_TYPE_RGBA,
344 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
345 PNG_FILTER_TYPE_DEFAULT);
346 png_write_info(png, info);
347
348 std::vector<png_bytep> rows(static_cast<size_t>(height));
349 for (int row = 0; row < height; ++row) {
350 rows[static_cast<size_t>(row)] = const_cast<png_bytep>(
351 rgba.data() + static_cast<size_t>(row) * width * 4);
352 }
353 png_write_image(png, rows.data());
354 png_write_end(png, nullptr);
355 png_destroy_write_struct(&png, &info);
356
357 return output;
358}
359#else
360absl::StatusOr<std::vector<uint8_t>> RenderService::EncodePng(
361 const std::vector<uint8_t>& /*rgba*/, int /*width*/, int /*height*/) {
362 return absl::UnimplementedError("PNG encoding unavailable (libpng missing)");
363}
364#endif
365
366} // namespace service
367} // namespace app
368} // 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:28
bool is_loaded() const
Definition rom.h:155
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)
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:69
const uint8_t * data() const
Definition bitmap.h:400
int height() const
Definition bitmap.h:397
int width() const
Definition bitmap.h:396
SDL_Surface * surface() const
Definition bitmap.h:402
RoomLayerManager - Manages layer visibility and compositing.
void ApplyLayerMerging(const LayerMergeType &merge_type)
void ApplyRoomEffect(EffectKey effect)
const CustomCollisionMap & custom_collision() const
Definition room.h:519
uint16_t message_id() const
Definition room.h:963
uint8_t blockset() const
Definition room.h:951
TagKey tag2() const
Definition room.h:937
uint8_t palette() const
Definition room.h:954
void RenderRoomGraphics()
Definition room.cc:1122
TagKey tag1() const
Definition room.h:936
CollisionKey collision() const
Definition room.h:938
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:876
gfx::Bitmap & GetCompositeBitmap(RoomLayerManager &layer_mgr)
Get a composite bitmap of all layers merged.
Definition room.cc:1098
uint8_t spriteset() const
Definition room.h:953
const std::vector< zelda3::Sprite > & GetSprites() const
Definition room.h:276
const std::vector< RoomObject > & GetTileObjects() const
Definition room.h:405
const LayerMergeType & layer_merging() const
Definition room.h:939
EffectKey effect() const
Definition room.h:935
void LoadSprites()
Definition room.cc:2629
uint8_t layout_id() const
Definition room.h:961
bool has_custom_collision() const
Definition room.h:523
void SetGameData(GameData *data)
Definition room.h:1012
absl::StatusOr< float > ParseRenderScale(absl::string_view value)
SDL_Palette * GetSurfacePalette(SDL_Surface *surface)
Get the palette attached to a surface.
Definition sdl_compat.h:392
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:648
constexpr int kNumberOfRooms
SDL2/SDL3 compatibility layer.
std::vector< uint8_t > png_data
std::array< uint8_t, 64 *64 > tiles