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
210 // Draw the object (ObjectDrawer needs the full palette group)
211 auto status =
212 drawer.DrawObject(obj, bg1_buffer, bg2_buffer, dungeon_main_pal_group);
213 if (!status.ok()) {
214 result.success = false;
215 result.error = std::string(status.message());
216 return result;
217 }
218
219 // Create output bitmap
220 std::vector<uint8_t> rgba_pixels(req.output_width * req.output_height * 4, 0);
221
222 // Ensure bitmaps are initialized before accessing pixel data
223 bg1_buffer.EnsureBitmapInitialized();
224 bg2_buffer.EnsureBitmapInitialized();
225
226 // Composite BG1 and BG2 into output
227 // BG2 is drawn first (lower priority), then BG1
228 const auto& bg2_pixels = bg2_buffer.bitmap().data();
229 const auto& bg1_pixels = bg1_buffer.bitmap().data();
230
231 for (int y = 0; y < req.output_height && y < 512; ++y) {
232 for (int x = 0; x < req.output_width && x < 512; ++x) {
233 int src_idx = y * 512 + x;
234 int dst_idx = (y * req.output_width + x) * 4;
235
236 uint8_t pixel = bg1_pixels[src_idx];
237 if (pixel == 0) {
238 pixel = bg2_pixels[src_idx];
239 }
240
241 // Convert indexed color to RGBA using palette
242 if (pixel > 0 && pixel < palette.size()) {
243 auto color = palette[pixel];
244 rgba_pixels[dst_idx + 0] = color.rgb().x; // R
245 rgba_pixels[dst_idx + 1] = color.rgb().y; // G
246 rgba_pixels[dst_idx + 2] = color.rgb().z; // B
247 rgba_pixels[dst_idx + 3] = 255; // A
248 } else {
249 // Transparent
250 rgba_pixels[dst_idx + 3] = 0;
251 }
252 }
253 }
254
255 result.rgba_pixels = std::move(rgba_pixels);
256 result.width = req.output_width;
257 result.height = req.output_height;
258 result.success = true;
259 result.used_static_fallback = true;
260
261 return result;
262}
263
264absl::StatusOr<RenderResult> EmulatorRenderService::RenderSprite(
265 const RenderRequest& req) {
266 RenderResult result;
267 result.success = false;
268 result.error = "Sprite rendering not yet implemented";
269 return result;
270}
271
272absl::StatusOr<RenderResult> EmulatorRenderService::RenderFullRoom(
273 const RenderRequest& req) {
274 RenderResult result;
275 result.success = false;
276 result.error = "Full room rendering not yet implemented";
277 return result;
278}
279
280void EmulatorRenderService::InjectRoomContext(int room_id, uint8_t blockset,
281 uint8_t palette) {
282 auto& ppu = snes_->ppu();
283
284 // Load room for graphics
286 room.SetGameData(game_data_); // Ensure room has access to GameData
287
288 // Load palette into CGRAM using the same row layout as vanilla:
289 // HUD rows 0-1, dungeon rows 2-7 starting at color $21.
290 if (!game_data_)
291 return;
292 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
293 if (palette < dungeon_main_pal_group.size()) {
294 auto base_palette = dungeon_main_pal_group[palette];
295 std::optional<gfx::SnesPalette> hud_palette_storage;
296 const gfx::SnesPalette* hud_palette = nullptr;
298 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
299 hud_palette = &*hud_palette_storage;
300 }
301 zelda3::LoadDungeonRenderPaletteToCgram(ppu.cgram, base_palette,
302 hud_palette);
303 }
304
305 // Load sprite auxiliary palettes (palettes 6-7, indices 90-119)
306 const uint32_t kSpriteAuxPc = SnesToPc(rom_addresses::kSpriteAuxPalettes);
307 for (int i = 0; i < 30; ++i) {
308 uint32_t addr = kSpriteAuxPc + i * 2;
309 if (addr + 1 < rom_->size()) {
310 uint16_t snes_color = rom_->data()[addr] | (rom_->data()[addr + 1] << 8);
311 ppu.cgram[90 + i] = snes_color;
312 }
313 }
314
315 // Load graphics into VRAM
316 if (blockset == 0xFF) {
317 room.LoadRoomGraphics();
318 } else {
319 room.LoadRoomGraphics(blockset);
320 }
322 const auto& gfx_buffer = room.get_gfx_buffer();
323
324 // Convert to SNES planar format
325 std::vector<uint8_t> linear_data(gfx_buffer.begin(), gfx_buffer.end());
326 auto planar_data = ConvertLinear8bppToPlanar4bpp(linear_data);
327
328 // Copy to VRAM
329 for (size_t i = 0; i < planar_data.size() / 2 && i < 0x8000; ++i) {
330 ppu.vram[i] = planar_data[i * 2] | (planar_data[i * 2 + 1] << 8);
331 }
332
333 // Setup PPU registers
334 snes_->Write(0x002105, 0x09); // BG Mode 1
335 snes_->Write(0x002107, 0x40); // BG1 tilemap at VRAM $4000
336 snes_->Write(0x002108, 0x48); // BG2 tilemap at VRAM $4800
337 snes_->Write(0x00210B, 0x00); // BG1/2 chr at VRAM $0000
338 snes_->Write(0x00212C, 0x03); // Enable BG1+BG2
339 snes_->Write(0x002100, 0x0F); // Full brightness
340
341 // Set room ID in WRAM
342 snes_->Write(wram_addresses::kRoomId, room_id & 0xFF);
343 snes_->Write(wram_addresses::kRoomId + 1, (room_id >> 8) & 0xFF);
344}
345
347 auto& ppu = snes_->ppu();
348 if (!game_data_)
349 return;
350 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
351
352 if (palette_id >= 0 &&
353 palette_id < static_cast<int>(dungeon_main_pal_group.size())) {
354 auto palette = dungeon_main_pal_group[palette_id];
355 std::optional<gfx::SnesPalette> hud_palette_storage;
356 const gfx::SnesPalette* hud_palette = nullptr;
358 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
359 hud_palette = &*hud_palette_storage;
360 }
361 zelda3::LoadDungeonRenderPaletteToCgram(ppu.cgram, palette, hud_palette);
362 }
363}
364
366 // This is handled by InjectRoomContext for now
367}
368
370 // Initialize the 11 tilemap indirect pointers at $BF-$DD
371 for (int i = 0; i < 11; ++i) {
372 uint32_t wram_addr = wram_addresses::kBG1TilemapBuffer +
374 uint8_t lo = wram_addr & 0xFF;
375 uint8_t mid = (wram_addr >> 8) & 0xFF;
376 uint8_t hi = (wram_addr >> 16) & 0xFF;
377
378 uint8_t zp_addr = wram_addresses::kTilemapPointers[i];
379 snes_->Write(0x7E0000 | zp_addr, lo);
380 snes_->Write(0x7E0000 | (zp_addr + 1), mid);
381 snes_->Write(0x7E0000 | (zp_addr + 2), hi);
382 }
383}
384
386 for (uint32_t i = 0; i < wram_addresses::kTilemapBufferSize; i++) {
389 }
390}
391
393 auto& apu = snes_->apu();
394 apu.out_ports_[0] = 0xAA; // Ready signal
395 apu.out_ports_[1] = 0xBB;
396 apu.out_ports_[2] = 0x00;
397 apu.out_ports_[3] = 0x00;
398}
399
401 int object_id, int* data_offset) {
402 auto rom_data = rom_->data();
403 uint32_t data_table_snes = 0;
404 uint32_t handler_table_snes = 0;
405
406 if (object_id < 0x100) {
407 data_table_snes = rom_addresses::kType1DataTable + (object_id * 2);
408 handler_table_snes = rom_addresses::kType1HandlerTable + (object_id * 2);
409 } else if (object_id < 0x200) {
410 data_table_snes =
411 rom_addresses::kType2DataTable + ((object_id - 0x100) * 2);
412 handler_table_snes =
413 rom_addresses::kType2HandlerTable + ((object_id - 0x100) * 2);
414 } else {
415 data_table_snes =
416 rom_addresses::kType3DataTable + ((object_id - 0x200) * 2);
417 handler_table_snes =
418 rom_addresses::kType3HandlerTable + ((object_id - 0x200) * 2);
419 }
420
421 uint32_t data_table_pc = SnesToPc(data_table_snes);
422 uint32_t handler_table_pc = SnesToPc(handler_table_snes);
423
424 if (data_table_pc + 1 >= rom_->size() ||
425 handler_table_pc + 1 >= rom_->size()) {
426 return absl::OutOfRangeError("Object ID out of bounds");
427 }
428
429 *data_offset = rom_data[data_table_pc] | (rom_data[data_table_pc + 1] << 8);
430 int handler_addr =
431 rom_data[handler_table_pc] | (rom_data[handler_table_pc + 1] << 8);
432
433 if (handler_addr == 0x0000) {
434 return absl::NotFoundError("Object has no drawing routine");
435 }
436
437 return handler_addr;
438}
439
440absl::Status EmulatorRenderService::ExecuteHandler(int handler_addr,
441 int data_offset,
442 int tilemap_pos) {
443 auto& cpu = snes_->cpu();
444
445 // Setup CPU state
446 cpu.PB = 0x01; // Program bank
447 cpu.DB = 0x7E; // Data bank (WRAM)
448 cpu.D = 0x0000; // Direct page
449 cpu.SetSP(0x01FF); // Stack
450 cpu.status = 0x30; // M=1, X=1 (8-bit mode)
451 cpu.E = 0; // Native mode
452
453 cpu.X = data_offset;
454 cpu.Y = tilemap_pos;
455
456 // Setup STP trap for return detection
457 const uint16_t trap_addr = 0xFF00;
458 snes_->Write(0x01FF00, 0xDB); // STP opcode
459
460 // Push return address
461 uint16_t sp = cpu.SP();
462 snes_->Write(0x010000 | sp--, 0x01);
463 snes_->Write(0x010000 | sp--, (trap_addr - 1) >> 8);
464 snes_->Write(0x010000 | sp--, (trap_addr - 1) & 0xFF);
465 cpu.SetSP(sp);
466
467 cpu.PC = handler_addr;
468
469 // Execute until STP or timeout
470 int max_opcodes = 100000;
471 int opcodes = 0;
472 auto& apu = snes_->apu();
473
474 while (opcodes < max_opcodes) {
475 uint32_t current_addr = (cpu.PB << 16) | cpu.PC;
476 uint8_t current_opcode = snes_->Read(current_addr);
477 if (current_opcode == 0xDB) {
478 break;
479 }
480
481 // Refresh APU mock periodically
482 if ((opcodes & 0x3F) == 0) {
483 apu.out_ports_[0] = 0xAA;
484 apu.out_ports_[1] = 0xBB;
485 }
486
487 cpu.RunOpcode();
488 opcodes++;
489 }
490
491 if (opcodes >= max_opcodes) {
492 return absl::DeadlineExceededError("Handler execution timeout");
493 }
494
495 return absl::OkStatus();
496}
497
499 auto& ppu = snes_->ppu();
500
501 // Copy WRAM tilemaps to VRAM
502 for (uint32_t i = 0; i < 0x800; i++) {
503 uint8_t lo = snes_->Read(wram_addresses::kBG1TilemapBuffer + i * 2);
504 uint8_t hi = snes_->Read(wram_addresses::kBG1TilemapBuffer + i * 2 + 1);
505 ppu.vram[0x4000 + i] = lo | (hi << 8);
506 }
507 for (uint32_t i = 0; i < 0x800; i++) {
508 uint8_t lo = snes_->Read(wram_addresses::kBG2TilemapBuffer + i * 2);
509 uint8_t hi = snes_->Read(wram_addresses::kBG2TilemapBuffer + i * 2 + 1);
510 ppu.vram[0x4800 + i] = lo | (hi << 8);
511 }
512
513 // Render frame
514 ppu.HandleFrameStart();
515 for (int line = 0; line < 224; line++) {
516 ppu.RunLine(line);
517 }
518 ppu.HandleVblank();
519}
520
522 // The SNES has a 512x478 framebuffer, but we typically render 256x224
523 std::vector<uint8_t> rgba(256 * 224 * 4);
524
525 // Get pixels from PPU's pixel buffer
526 // PPU stores pixels in 16-bit SNES format, need to convert to RGBA
527 auto& ppu = snes_->ppu();
528
529 for (int y = 0; y < 224; ++y) {
530 for (int x = 0; x < 256; ++x) {
531 int idx = (y * 256 + x) * 4;
532
533 // Get the SNES color from the PPU's output
534 // This assumes the PPU has already rendered to its internal buffer
535 // For now, just fill with test pattern
536 rgba[idx + 0] = 0; // R
537 rgba[idx + 1] = 0; // G
538 rgba[idx + 2] = 0; // B
539 rgba[idx + 3] = 255; // A
540 }
541 }
542
543 return rgba;
544}
545
546} // namespace render
547} // namespace emu
548} // 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:155
auto data() const
Definition rom.h:151
auto size() const
Definition rom.h:150
bool is_loaded() const
Definition rom.h:144
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:970
void CopyRoomGraphicsToBuffer()
Definition room.cc:874
uint8_t palette() const
Definition room.h:905
void LoadRoomGraphics(std::optional< uint8_t > entrance_blockset=std::nullopt)
Definition room.cc:746
void SetGameData(GameData *data)
Definition room.h:963
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:118
Room LoadRoomFromRom(Rom *rom, int room_id)
Definition room.cc:518
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)