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