yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
emulator_render_service.cc
Go to the documentation of this file.
2
3#include <cstdio>
4#include <optional>
5
7#include "app/emu/snes.h"
8#include "rom/rom.h"
10#include "zelda3/dungeon/room.h"
12#include "zelda3/game_data.h"
13
14namespace yaze {
15namespace emu {
16namespace render {
17
19 zelda3::GameData* game_data)
20 : rom_(rom), game_data_(game_data) {}
21
23
25 if (!rom_ || !rom_->is_loaded()) {
26 return absl::FailedPreconditionError("ROM not loaded");
27 }
28
29 if (!game_data_) {
30 owned_game_data_ = std::make_unique<zelda3::GameData>(rom_);
31 zelda3::LoadOptions options;
32 options.load_graphics = true;
33 options.load_palettes = true;
34 options.load_gfx_groups = true;
35 options.expand_rom = false;
36 options.populate_metadata = true;
37 auto data_status = zelda3::LoadGameData(*rom_, *owned_game_data_, options);
38 if (!data_status.ok()) {
39 return data_status;
40 }
42 }
43
44 // Create SNES instance
45 snes_ = std::make_unique<emu::Snes>();
46 const std::vector<uint8_t>& rom_data = rom_->vector();
47 snes_->Init(rom_data);
48
49 // Create save state manager
50 state_manager_ = std::make_unique<SaveStateManager>(snes_.get(), rom_);
51 auto status = state_manager_->Initialize();
52 if (!status.ok()) {
53 return status;
54 }
55
56 initialized_ = true;
57 return absl::OkStatus();
58}
59
61 if (!state_manager_) {
62 return absl::FailedPreconditionError("Service not initialized");
63 }
64 return state_manager_->GenerateAllBaselineStates();
65}
66
67absl::StatusOr<RenderResult> EmulatorRenderService::Render(
68 const RenderRequest& request) {
69 if (!initialized_) {
70 return absl::FailedPreconditionError("Service not initialized");
71 }
72
73 switch (request.type) {
77 return RenderDungeonObjectStatic(request);
78 }
79 return RenderDungeonObject(request);
80
82 return RenderSprite(request);
83
85 return RenderFullRoom(request);
86
87 default:
88 return absl::InvalidArgumentError("Unknown render target type");
89 }
90}
91
92absl::StatusOr<std::vector<RenderResult>> EmulatorRenderService::RenderBatch(
93 const std::vector<RenderRequest>& requests) {
94 std::vector<RenderResult> results;
95 results.reserve(requests.size());
96
97 for (const auto& request : requests) {
98 auto result = Render(request);
99 if (result.ok()) {
100 results.push_back(std::move(*result));
101 } else {
102 RenderResult error_result;
103 error_result.success = false;
104 error_result.error = std::string(result.status().message());
105 results.push_back(std::move(error_result));
106 }
107 }
108
109 return results;
110}
111
113 const RenderRequest& req) {
114 RenderResult result;
115
116 // Load baseline room state
117 auto status = state_manager_->LoadState(StateType::kRoomLoaded, req.room_id);
118 if (!status.ok()) {
119 // Fall back to cold start if no state available
120 snes_->Reset(true);
121 }
122
123 // Load room context
125
126 // Inject room context
128 req.use_room_defaults ? room.palette() : req.palette);
129
130 // Clear tilemap buffers
132
133 // Initialize tilemap pointers
135
136 // Mock APU ports
137 MockApuPorts();
138
139 // Lookup handler address
140 int data_offset = 0;
141 auto handler_result = LookupHandlerAddress(req.entity_id, &data_offset);
142 if (!handler_result.ok()) {
143 result.success = false;
144 result.error = std::string(handler_result.status().message());
145 return result;
146 }
147 int handler_addr = *handler_result;
148
149 // Calculate tilemap position
150 int tilemap_pos = (req.y * 0x80) + (req.x * 2);
151
152 // Execute handler
153 status = ExecuteHandler(handler_addr, data_offset, tilemap_pos);
154 if (!status.ok()) {
155 result.success = false;
156 result.error = std::string(status.message());
157 return result;
158 }
159
160 // Render PPU frame and extract pixels
163 result.width = 256;
164 result.height = 224;
165 result.success = true;
166 result.handler_address = handler_addr;
167
168 return result;
169}
170
172 const RenderRequest& req) {
173 RenderResult result;
174
175 // Load room for context
177 room.SetGameData(game_data_); // Ensure room has access to GameData
178
179 // Load room graphics
180 if (req.use_room_defaults) {
181 room.LoadRoomGraphics();
182 } else {
183 room.LoadRoomGraphics(req.blockset);
184 }
186
187 // Get palette group and specific palette for color conversion
188 if (!game_data_) {
189 return absl::FailedPreconditionError("GameData not available");
190 }
191 auto& dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
192 uint8_t palette_id = req.use_room_defaults ? room.palette() : req.palette;
193 if (palette_id >= dungeon_main_pal_group.size()) {
194 palette_id = 0;
195 }
196 auto palette = dungeon_main_pal_group[palette_id]; // For RGBA conversion
197
198 // Create background buffers for rendering
199 gfx::BackgroundBuffer bg1_buffer(512, 512);
200 gfx::BackgroundBuffer bg2_buffer(512, 512);
201
202 // Create object
203 zelda3::RoomObject obj(req.entity_id, req.x, req.y, req.size, 0);
204
205 // Create object drawer with room graphics buffer
206 const auto& gfx_buffer = room.get_gfx_buffer();
207 zelda3::ObjectDrawer drawer(rom_, req.room_id, gfx_buffer.data());
208 drawer.InitializeDrawRoutines();
209 drawer.SetRoomFloorGraphics(room.floor1(), room.floor2());
210
211 // Draw the object (ObjectDrawer needs the full palette group)
212 auto status =
213 drawer.DrawObject(obj, bg1_buffer, bg2_buffer, dungeon_main_pal_group);
214 if (!status.ok()) {
215 result.success = false;
216 result.error = std::string(status.message());
217 return result;
218 }
219
220 // Create output bitmap
221 std::vector<uint8_t> rgba_pixels(req.output_width * req.output_height * 4, 0);
222
223 // Ensure bitmaps are initialized before accessing pixel data
224 bg1_buffer.EnsureBitmapInitialized();
225 bg2_buffer.EnsureBitmapInitialized();
226
227 // Composite BG1 and BG2 into output
228 // BG2 is drawn first (lower priority), then BG1
229 const auto& bg2_pixels = bg2_buffer.bitmap().data();
230 const auto& bg1_pixels = bg1_buffer.bitmap().data();
231
232 for (int y = 0; y < req.output_height && y < 512; ++y) {
233 for (int x = 0; x < req.output_width && x < 512; ++x) {
234 int src_idx = y * 512 + x;
235 int dst_idx = (y * req.output_width + x) * 4;
236
237 uint8_t pixel = bg1_pixels[src_idx];
238 if (pixel == 0) {
239 pixel = bg2_pixels[src_idx];
240 }
241
242 // Convert indexed color to RGBA using palette
243 if (pixel > 0 && pixel < palette.size()) {
244 auto color = palette[pixel];
245 rgba_pixels[dst_idx + 0] = color.rgb().x; // R
246 rgba_pixels[dst_idx + 1] = color.rgb().y; // G
247 rgba_pixels[dst_idx + 2] = color.rgb().z; // B
248 rgba_pixels[dst_idx + 3] = 255; // A
249 } else {
250 // Transparent
251 rgba_pixels[dst_idx + 3] = 0;
252 }
253 }
254 }
255
256 result.rgba_pixels = std::move(rgba_pixels);
257 result.width = req.output_width;
258 result.height = req.output_height;
259 result.success = true;
260 result.used_static_fallback = true;
261
262 return result;
263}
264
265absl::StatusOr<RenderResult> EmulatorRenderService::RenderSprite(
266 const RenderRequest& req) {
267 RenderResult result;
268 result.success = false;
269 result.error = "Sprite rendering not yet implemented";
270 return result;
271}
272
273absl::StatusOr<RenderResult> EmulatorRenderService::RenderFullRoom(
274 const RenderRequest& req) {
275 RenderResult result;
276 result.success = false;
277 result.error = "Full room rendering not yet implemented";
278 return result;
279}
280
281void EmulatorRenderService::InjectRoomContext(int room_id, uint8_t blockset,
282 uint8_t palette) {
283 auto& ppu = snes_->ppu();
284
285 // Load room for graphics
287 room.SetGameData(game_data_); // Ensure room has access to GameData
288
289 // Load palette into CGRAM using the same row layout as vanilla:
290 // HUD rows 0-1, dungeon rows 2-7 starting at color $21.
291 if (!game_data_)
292 return;
293 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
294 if (palette < dungeon_main_pal_group.size()) {
295 auto base_palette = dungeon_main_pal_group[palette];
296 std::optional<gfx::SnesPalette> hud_palette_storage;
297 const gfx::SnesPalette* hud_palette = nullptr;
299 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
300 hud_palette = &*hud_palette_storage;
301 }
302 zelda3::LoadDungeonRenderPaletteToCgram(ppu.cgram, base_palette,
303 hud_palette);
304 }
305
306 // Load sprite auxiliary palettes (palettes 6-7, indices 90-119)
307 const uint32_t kSpriteAuxPc = SnesToPc(rom_addresses::kSpriteAuxPalettes);
308 for (int i = 0; i < 30; ++i) {
309 uint32_t addr = kSpriteAuxPc + i * 2;
310 if (addr + 1 < rom_->size()) {
311 uint16_t snes_color = rom_->data()[addr] | (rom_->data()[addr + 1] << 8);
312 ppu.cgram[90 + i] = snes_color;
313 }
314 }
315
316 // Load graphics into VRAM
317 if (blockset == 0xFF) {
318 room.LoadRoomGraphics();
319 } else {
320 room.LoadRoomGraphics(blockset);
321 }
323 const auto& gfx_buffer = room.get_gfx_buffer();
324
325 // Convert to SNES planar format
326 std::vector<uint8_t> linear_data(gfx_buffer.begin(), gfx_buffer.end());
327 auto planar_data = ConvertLinear8bppToPlanar4bpp(linear_data);
328
329 // Copy to VRAM
330 for (size_t i = 0; i < planar_data.size() / 2 && i < 0x8000; ++i) {
331 ppu.vram[i] = planar_data[i * 2] | (planar_data[i * 2 + 1] << 8);
332 }
333
334 // Setup PPU registers
335 snes_->Write(0x002105, 0x09); // BG Mode 1
336 snes_->Write(0x002107, 0x40); // BG1 tilemap at VRAM $4000
337 snes_->Write(0x002108, 0x48); // BG2 tilemap at VRAM $4800
338 snes_->Write(0x00210B, 0x00); // BG1/2 chr at VRAM $0000
339 snes_->Write(0x00212C, 0x03); // Enable BG1+BG2
340 snes_->Write(0x002100, 0x0F); // Full brightness
341
342 // Set room ID in WRAM
343 snes_->Write(wram_addresses::kRoomId, room_id & 0xFF);
344 snes_->Write(wram_addresses::kRoomId + 1, (room_id >> 8) & 0xFF);
345}
346
348 auto& ppu = snes_->ppu();
349 if (!game_data_)
350 return;
351 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
352
353 if (palette_id >= 0 &&
354 palette_id < static_cast<int>(dungeon_main_pal_group.size())) {
355 auto palette = dungeon_main_pal_group[palette_id];
356 std::optional<gfx::SnesPalette> hud_palette_storage;
357 const gfx::SnesPalette* hud_palette = nullptr;
359 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
360 hud_palette = &*hud_palette_storage;
361 }
362 zelda3::LoadDungeonRenderPaletteToCgram(ppu.cgram, palette, hud_palette);
363 }
364}
365
367 // This is handled by InjectRoomContext for now
368}
369
371 // Initialize the 11 tilemap indirect pointers at $BF-$DD
372 for (int i = 0; i < 11; ++i) {
373 uint32_t wram_addr = wram_addresses::kBG1TilemapBuffer +
375 uint8_t lo = wram_addr & 0xFF;
376 uint8_t mid = (wram_addr >> 8) & 0xFF;
377 uint8_t hi = (wram_addr >> 16) & 0xFF;
378
379 uint8_t zp_addr = wram_addresses::kTilemapPointers[i];
380 snes_->Write(0x7E0000 | zp_addr, lo);
381 snes_->Write(0x7E0000 | (zp_addr + 1), mid);
382 snes_->Write(0x7E0000 | (zp_addr + 2), hi);
383 }
384}
385
387 for (uint32_t i = 0; i < wram_addresses::kTilemapBufferSize; i++) {
390 }
391}
392
394 auto& apu = snes_->apu();
395 apu.out_ports_[0] = 0xAA; // Ready signal
396 apu.out_ports_[1] = 0xBB;
397 apu.out_ports_[2] = 0x00;
398 apu.out_ports_[3] = 0x00;
399}
400
402 int object_id, int* data_offset) {
403 auto rom_data = rom_->data();
404 uint32_t data_table_snes = 0;
405 uint32_t handler_table_snes = 0;
406
407 if (object_id < 0x100) {
408 data_table_snes = rom_addresses::kType1DataTable + (object_id * 2);
409 handler_table_snes = rom_addresses::kType1HandlerTable + (object_id * 2);
410 } else if (object_id < 0x200) {
411 data_table_snes =
412 rom_addresses::kType2DataTable + ((object_id - 0x100) * 2);
413 handler_table_snes =
414 rom_addresses::kType2HandlerTable + ((object_id - 0x100) * 2);
415 } else {
416 data_table_snes =
417 rom_addresses::kType3DataTable + ((object_id - 0x200) * 2);
418 handler_table_snes =
419 rom_addresses::kType3HandlerTable + ((object_id - 0x200) * 2);
420 }
421
422 uint32_t data_table_pc = SnesToPc(data_table_snes);
423 uint32_t handler_table_pc = SnesToPc(handler_table_snes);
424
425 if (data_table_pc + 1 >= rom_->size() ||
426 handler_table_pc + 1 >= rom_->size()) {
427 return absl::OutOfRangeError("Object ID out of bounds");
428 }
429
430 *data_offset = rom_data[data_table_pc] | (rom_data[data_table_pc + 1] << 8);
431 int handler_addr =
432 rom_data[handler_table_pc] | (rom_data[handler_table_pc + 1] << 8);
433
434 if (handler_addr == 0x0000) {
435 return absl::NotFoundError("Object has no drawing routine");
436 }
437
438 return handler_addr;
439}
440
441absl::Status EmulatorRenderService::ExecuteHandler(int handler_addr,
442 int data_offset,
443 int tilemap_pos) {
444 auto& cpu = snes_->cpu();
445
446 // Setup CPU state
447 cpu.PB = 0x01; // Program bank
448 cpu.DB = 0x7E; // Data bank (WRAM)
449 cpu.D = 0x0000; // Direct page
450 cpu.SetSP(0x01FF); // Stack
451 cpu.status = 0x30; // M=1, X=1 (8-bit mode)
452 cpu.E = 0; // Native mode
453
454 cpu.X = data_offset;
455 cpu.Y = tilemap_pos;
456
457 // Setup STP trap for return detection
458 const uint16_t trap_addr = 0xFF00;
459 snes_->Write(0x01FF00, 0xDB); // STP opcode
460
461 // Push return address
462 uint16_t sp = cpu.SP();
463 snes_->Write(0x010000 | sp--, 0x01);
464 snes_->Write(0x010000 | sp--, (trap_addr - 1) >> 8);
465 snes_->Write(0x010000 | sp--, (trap_addr - 1) & 0xFF);
466 cpu.SetSP(sp);
467
468 cpu.PC = handler_addr;
469
470 // Execute until STP or timeout
471 int max_opcodes = 100000;
472 int opcodes = 0;
473 auto& apu = snes_->apu();
474
475 while (opcodes < max_opcodes) {
476 uint32_t current_addr = (cpu.PB << 16) | cpu.PC;
477 uint8_t current_opcode = snes_->Read(current_addr);
478 if (current_opcode == 0xDB) {
479 break;
480 }
481
482 // Refresh APU mock periodically
483 if ((opcodes & 0x3F) == 0) {
484 apu.out_ports_[0] = 0xAA;
485 apu.out_ports_[1] = 0xBB;
486 }
487
488 cpu.RunOpcode();
489 opcodes++;
490 }
491
492 if (opcodes >= max_opcodes) {
493 return absl::DeadlineExceededError("Handler execution timeout");
494 }
495
496 return absl::OkStatus();
497}
498
500 auto& ppu = snes_->ppu();
501
502 // Copy WRAM tilemaps to VRAM
503 for (uint32_t i = 0; i < 0x800; i++) {
504 uint8_t lo = snes_->Read(wram_addresses::kBG1TilemapBuffer + i * 2);
505 uint8_t hi = snes_->Read(wram_addresses::kBG1TilemapBuffer + i * 2 + 1);
506 ppu.vram[0x4000 + i] = lo | (hi << 8);
507 }
508 for (uint32_t i = 0; i < 0x800; i++) {
509 uint8_t lo = snes_->Read(wram_addresses::kBG2TilemapBuffer + i * 2);
510 uint8_t hi = snes_->Read(wram_addresses::kBG2TilemapBuffer + i * 2 + 1);
511 ppu.vram[0x4800 + i] = lo | (hi << 8);
512 }
513
514 // Render frame
515 ppu.HandleFrameStart();
516 for (int line = 0; line < 224; line++) {
517 ppu.RunLine(line);
518 }
519 ppu.HandleVblank();
520}
521
523 // The SNES has a 512x478 framebuffer, but we typically render 256x224
524 std::vector<uint8_t> rgba(256 * 224 * 4);
525
526 // Get pixels from PPU's pixel buffer
527 // PPU stores pixels in 16-bit SNES format, need to convert to RGBA
528 auto& ppu = snes_->ppu();
529
530 for (int y = 0; y < 224; ++y) {
531 for (int x = 0; x < 256; ++x) {
532 int idx = (y * 256 + x) * 4;
533
534 // Get the SNES color from the PPU's output
535 // This assumes the PPU has already rendered to its internal buffer
536 // For now, just fill with test pattern
537 rgba[idx + 0] = 0; // R
538 rgba[idx + 1] = 0; // G
539 rgba[idx + 2] = 0; // B
540 rgba[idx + 3] = 255; // A
541 }
542 }
543
544 return rgba;
545}
546
547} // namespace render
548} // namespace emu
549} // 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
const auto & vector() const
Definition rom.h:173
auto data() const
Definition rom.h:169
auto size() const
Definition rom.h:168
bool is_loaded() const
Definition rom.h:155
absl::StatusOr< RenderResult > RenderDungeonObjectStatic(const RenderRequest &req)
absl::Status ExecuteHandler(int handler_addr, int data_offset, int tilemap_pos)
absl::StatusOr< RenderResult > Render(const RenderRequest &request)
void InjectRoomContext(int room_id, uint8_t blockset, uint8_t palette)
absl::StatusOr< RenderResult > RenderDungeonObject(const RenderRequest &req)
absl::StatusOr< RenderResult > RenderFullRoom(const RenderRequest &req)
absl::StatusOr< std::vector< RenderResult > > RenderBatch(const std::vector< RenderRequest > &requests)
absl::StatusOr< RenderResult > RenderSprite(const RenderRequest &req)
std::unique_ptr< SaveStateManager > state_manager_
std::unique_ptr< zelda3::GameData > owned_game_data_
absl::StatusOr< int > LookupHandlerAddress(int object_id, int *data_offset)
EmulatorRenderService(Rom *rom, zelda3::GameData *game_data=nullptr)
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
Draws dungeon objects to background buffers using game patterns.
void InitializeDrawRoutines()
Initialize draw routine registry Must be called before drawing objects.
void SetRoomFloorGraphics(uint8_t floor1, uint8_t floor2)
absl::Status DrawObject(const RoomObject &object, gfx::BackgroundBuffer &bg1, gfx::BackgroundBuffer &bg2, const gfx::PaletteGroup &palette_group, const DungeonState *state=nullptr, gfx::BackgroundBuffer *layout_bg1=nullptr, gfx::BackgroundBuffer *layout_bg2=nullptr)
Draw a room object to background buffers.
const std::array< uint8_t, 0x10000 > & get_gfx_buffer() const
Definition room.h:1019
void CopyRoomGraphicsToBuffer()
Definition room.cc:995
uint8_t floor2() const
Definition room.h:976
uint8_t palette() const
Definition room.h:954
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:876
uint8_t floor1() const
Definition room.h:975
void SetGameData(GameData *data)
Definition room.h:1012
constexpr uint32_t kType3HandlerTable
constexpr uint32_t kType2HandlerTable
constexpr uint32_t kSpriteAuxPalettes
constexpr uint32_t kType1HandlerTable
uint32_t SnesToPc(uint32_t snes_addr)
std::vector< uint8_t > ConvertLinear8bppToPlanar4bpp(const std::vector< uint8_t > &linear_data)
absl::Status LoadGameData(Rom &rom, GameData &data, const LoadOptions &options)
Loads all Zelda3-specific game data from a generic ROM.
Definition game_data.cc:123
void LoadDungeonRenderPaletteToCgram(std::span< uint16_t > cgram, const gfx::SnesPalette &dungeon_palette, const gfx::SnesPalette *hud_palette)
Definition room.cc:248
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:648
SNES color in 15-bit RGB format (BGR555)
std::vector< uint8_t > rgba_pixels
const SnesPalette & palette_ref(int i) const
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
struct snes_color snes_color
SNES color in 15-bit RGB format (BGR555)