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