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