yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
dungeon_object_emulator_preview.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <cstdio>
5#include <cstring>
6#include <optional>
7
15#include "app/platform/window.h"
17#include "zelda3/dungeon/room.h"
19
20using namespace yaze::editor;
21
22namespace {
23
24// Convert 8BPP linear tile data to 4BPP SNES planar format
25// Input: 64 bytes per tile (1 byte per pixel, linear row-major order)
26// Output: 32 bytes per tile (4 bitplanes interleaved per SNES 4BPP format)
27std::vector<uint8_t> ConvertLinear8bppToPlanar4bpp(
28 const std::vector<uint8_t>& linear_data) {
29 size_t num_tiles = linear_data.size() / 64; // 64 bytes per 8x8 tile
30 std::vector<uint8_t> planar_data(num_tiles * 32); // 32 bytes per tile
31
32 for (size_t tile = 0; tile < num_tiles; ++tile) {
33 const uint8_t* src = linear_data.data() + tile * 64;
34 uint8_t* dst = planar_data.data() + tile * 32;
35
36 for (int row = 0; row < 8; ++row) {
37 uint8_t bp0 = 0, bp1 = 0, bp2 = 0, bp3 = 0;
38
39 for (int col = 0; col < 8; ++col) {
40 uint8_t pixel = src[row * 8 + col] & 0x0F; // Low 4 bits only
41 int bit = 7 - col; // MSB first
42
43 bp0 |= ((pixel >> 0) & 1) << bit;
44 bp1 |= ((pixel >> 1) & 1) << bit;
45 bp2 |= ((pixel >> 2) & 1) << bit;
46 bp3 |= ((pixel >> 3) & 1) << bit;
47 }
48
49 // SNES 4BPP interleaving: bp0,bp1 for rows 0-7 first, then bp2,bp3
50 dst[row * 2] = bp0;
51 dst[row * 2 + 1] = bp1;
52 dst[16 + row * 2] = bp2;
53 dst[16 + row * 2 + 1] = bp3;
54 }
55 }
56
57 return planar_data;
58}
59
60// Convert SNES LoROM address to PC (file) offset
61// ALTTP uses LoROM mapping:
62// - Banks $00-$3F: Address $8000-$FFFF maps to ROM
63// - Each bank contributes 32KB ($8000 bytes) of ROM data
64// - PC = (bank & 0x7F) * 0x8000 + (addr - 0x8000)
65// Takes a 24-bit SNES address (e.g., 0x018200 = bank $01, addr $8200)
66uint32_t SnesToPc(uint32_t snes_addr) {
67 uint8_t bank = (snes_addr >> 16) & 0xFF;
68 uint16_t addr = snes_addr & 0xFFFF;
69
70 // LoROM: banks $00-$3F map to ROM ($8000-$FFFF only)
71 // Each bank = 32KB of ROM, so multiply bank by 0x8000
72 // Formula: PC = (bank & 0x7F) * 0x8000 + (addr - 0x8000)
73 if (addr >= 0x8000) {
74 return (bank & 0x7F) * 0x8000 + (addr - 0x8000);
75 }
76 // For addresses below $8000, return as-is (WRAM/hardware regs)
77 return snes_addr;
78}
79
80} // namespace
81
82namespace yaze {
83namespace gui {
84
86 // Defer SNES initialization until actually needed to reduce startup memory
87}
88
90 // if (object_texture_) {
91 // renderer_->DestroyTexture(object_texture_);
92 // }
93}
94
96 gfx::IRenderer* renderer, Rom* rom, zelda3::GameData* game_data,
97 emu::render::EmulatorRenderService* render_service) {
98 renderer_ = renderer;
99 rom_ = rom;
100 game_data_ = game_data;
101 render_service_ = render_service;
102 // Defer SNES initialization until EnsureInitialized() is called
103 // This avoids a ~2MB ROM copy during startup
104 // object_texture_ = renderer_->CreateTexture(256, 256);
105}
106
108 if (initialized_)
109 return;
110 if (!rom_ || !rom_->is_loaded())
111 return;
112
113 snes_instance_ = std::make_unique<emu::Snes>();
114 // Use const reference to avoid copying the ROM data
115 const std::vector<uint8_t>& rom_data = rom_->vector();
116 snes_instance_->Init(rom_data);
117
118 // Create texture for rendering output
119 if (renderer_ && !object_texture_) {
121 }
122
123 initialized_ = true;
124}
125
127 if (!show_window_)
128 return;
129
130 const auto& theme = AgentUI::GetTheme();
131
132 // No window creation - embedded in parent
133 {
134 AutoWidgetScope scope("DungeonEditor/EmulatorPreview");
135
136 // ROM status indicator at top
137 if (rom_ && rom_->is_loaded()) {
138 ImGui::TextColored(theme.status_success, tr("ROM: Loaded"));
139 ImGui::SameLine();
140 ImGui::TextDisabled(tr("Ready to render objects"));
141 } else {
142 ImGui::TextColored(theme.status_error, tr("ROM: Not loaded"));
143 ImGui::SameLine();
144 ImGui::TextDisabled(tr("Load a ROM to use this tool"));
145 }
146
147 ImGui::Separator();
148
149 // Vertical layout for narrow panels
151
153 ImGui::Separator();
154
155 // Preview image with border
157 ImGui::BeginChild("PreviewRegion", ImVec2(0, 280), true,
158 ImGuiWindowFlags_NoScrollbar);
159 ImGui::TextColored(theme.text_info, tr("Preview"));
160 ImGui::Separator();
161 if (object_texture_) {
162 ImVec2 available = ImGui::GetContentRegionAvail();
163 float scale = std::min(available.x / 256.0f, available.y / 256.0f);
164 ImVec2 preview_size(256 * scale, 256 * scale);
165
166 // Center the preview
167 float offset_x = (available.x - preview_size.x) * 0.5f;
168 if (offset_x > 0)
169 ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offset_x);
170
171 ImGui::Image((ImTextureID)object_texture_, preview_size);
172 } else {
173 ImGui::TextColored(theme.text_warning_yellow, tr("No texture available"));
174 ImGui::TextWrapped(tr("Click 'Render Object' to generate a preview"));
175 }
176 ImGui::EndChild();
178
180
181 // Status panel
183
184 // Help text at bottom
186 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.box_bg_dark);
187 ImGui::BeginChild("HelpText", ImVec2(0, 0), true);
188 ImGui::TextColored(theme.text_info, tr("How it works:"));
189 ImGui::Separator();
190 ImGui::TextWrapped(tr(
191 "This tool uses the SNES emulator to render objects by executing the "
192 "game's native drawing routines from bank $01. This provides accurate "
193 "previews of how objects will appear in-game."));
194 ImGui::EndChild();
195 ImGui::PopStyleColor();
196 }
197
198 // Render object browser if visible
199 if (show_browser_) {
201 }
202}
203
205 const auto& theme = AgentUI::GetTheme();
206
207 // Object ID section with name lookup
208 ImGui::TextColored(theme.text_info, tr("Object Selection"));
209 ImGui::Separator();
210
211 // Object ID input with hex display
212 AutoInputInt("Object ID", &object_id_, 1, 10,
213 ImGuiInputTextFlags_CharsHexadecimal);
214 ImGui::SameLine();
215 ImGui::TextColored(theme.text_secondary_gray, "($%03X)", object_id_);
216
217 // Display object name and type
218 const char* name = GetObjectName(object_id_);
219 int type = GetObjectType(object_id_);
220
221 ImGui::PushStyleColor(ImGuiCol_ChildBg, theme.panel_bg_darker);
222 ImGui::BeginChild("ObjectInfo", ImVec2(0, 60), true);
223 ImGui::TextColored(theme.accent_color, tr("Name:"));
224 ImGui::SameLine();
225 ImGui::TextWrapped("%s", name);
226 ImGui::TextColored(theme.accent_color, tr("Type:"));
227 ImGui::SameLine();
228 ImGui::Text("%d", type);
229 ImGui::EndChild();
230 ImGui::PopStyleColor();
231
233
234 // Quick select dropdown
235 if (ImGui::BeginCombo(tr("Quick Select"), "Choose preset...")) {
236 for (const auto& preset : kQuickPresets) {
237 if (ImGui::Selectable(preset.name, object_id_ == preset.id)) {
238 object_id_ = preset.id;
239 }
240 if (object_id_ == preset.id) {
241 ImGui::SetItemDefaultFocus();
242 }
243 }
244 ImGui::EndCombo();
245 }
246
248
249 // Browse button for full object list
250 if (AgentUI::StyledButton("Browse All Objects...", theme.accent_color,
251 ImVec2(-1, 0))) {
253 }
254
256 ImGui::Separator();
257
258 // Position and size controls
259 ImGui::TextColored(theme.text_info, tr("Position & Size"));
260 ImGui::Separator();
261
262 AutoSliderInt("X Position", &object_x_, 0, 63);
263 AutoSliderInt("Y Position", &object_y_, 0, 63);
264 AutoSliderInt("Size", &object_size_, 0, 15);
265 ImGui::SameLine();
266 ImGui::TextDisabled("(?)");
267 if (ImGui::IsItemHovered()) {
268 ImGui::SetTooltip(
269 tr("Size parameter for scalable objects.\nMany objects ignore this "
270 "value."));
271 }
272
274 ImGui::Separator();
275
276 // Room context
277 ImGui::TextColored(theme.text_info, tr("Rendering Context"));
278 ImGui::Separator();
279
280 AutoInputInt("Room ID", &room_id_, 1, 10);
281 ImGui::SameLine();
282 ImGui::TextDisabled("(?)");
283 if (ImGui::IsItemHovered()) {
284 ImGui::SetTooltip(tr("Room ID for graphics and palette context"));
285 }
286
288
289 // Render mode selector
290 ImGui::TextColored(theme.text_info, tr("Render Mode"));
291 int mode = static_cast<int>(render_mode_);
292 if (ImGui::RadioButton(tr("Static (ObjectDrawer)"), &mode, 0)) {
295 }
296 ImGui::SameLine();
297 ImGui::TextDisabled("(?)");
298 if (ImGui::IsItemHovered()) {
299 ImGui::SetTooltip(
300 tr("Uses ObjectDrawer to render objects.\n"
301 "This is the reliable method that matches the main canvas."));
302 }
303 if (ImGui::RadioButton(tr("Emulator (Experimental)"), &mode, 1)) {
305 }
306 ImGui::SameLine();
307 ImGui::TextDisabled("(?)");
308 if (ImGui::IsItemHovered()) {
309 ImGui::SetTooltip(
310 tr("Attempts to run game drawing handlers via CPU emulation.\n"
311 "EXPERIMENTAL: Handlers require full game state to work.\n"
312 "Most objects will time out without rendering."));
313 }
314
316
317 // Render button - large and prominent
318 if (AgentUI::StyledButton("Render Object", theme.status_success,
319 ImVec2(-1, 40))) {
322 } else {
324 }
325 }
326}
327
329 if (!rom_ || !rom_->is_loaded()) {
330 last_error_ = "ROM not loaded";
331 return;
332 }
333
334 // Use shared render service if available (set to emulated mode)
336 // Temporarily switch to emulated mode
337 auto prev_mode = render_service_->GetRenderMode();
339
342 request.entity_id = object_id_;
343 request.x = object_x_;
344 request.y = object_y_;
345 request.size = object_size_;
346 request.room_id = room_id_;
347 request.output_width = 256;
348 request.output_height = 256;
349
350 auto result = render_service_->Render(request);
351
352 // Restore previous mode
353 render_service_->SetRenderMode(prev_mode);
354
355 if (result.ok() && result->success) {
356 last_cycle_count_ = result->cycles_executed;
357 // Update texture with rendered pixels
358 if (!object_texture_) {
360 }
361 void* pixels = nullptr;
362 int pitch = 0;
363 if (renderer_->LockTexture(object_texture_, nullptr, &pixels, &pitch)) {
364 memcpy(pixels, result->rgba_pixels.data(), result->rgba_pixels.size());
366 }
367 printf("[SERVICE-EMU] Rendered object $%04X via EmulatorRenderService\n",
368 object_id_);
369 return;
370 } else {
371 printf(
372 "[SERVICE-EMU] Emulated render failed, falling back to legacy: %s\n",
373 result.ok() ? result->error.c_str()
374 : std::string(result.status().message()).c_str());
375 }
376 }
377
378 // Legacy emulated rendering path
379 // Lazy initialize the SNES emulator on first use
381 if (!snes_instance_) {
382 last_error_ = "Failed to initialize SNES emulator";
383 return;
384 }
385
386 last_error_.clear();
388
389 // 1. Reset and configure the SNES state
390 snes_instance_->Reset(true);
391 auto& cpu = snes_instance_->cpu();
392 auto& ppu = snes_instance_->ppu();
393 auto& memory = snes_instance_->memory();
394
395 // 2. Load room context (graphics, palettes)
397 default_room.SetGameData(game_data_); // Ensure room has access to GameData
398
399 // 3. Load palette into CGRAM (full 120 colors including sprite aux)
400 if (!game_data_) {
401 last_error_ = "GameData not available";
402 return;
403 }
404 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
405
406 // Validate and clamp palette ID
407 int palette_id = default_room.palette();
408 if (palette_id < 0 ||
409 palette_id >= static_cast<int>(dungeon_main_pal_group.size())) {
410 printf("[EMU] Warning: Room palette %d out of bounds, using palette 0\n",
411 palette_id);
412 palette_id = 0;
413 }
414
415 // Load dungeon palette rows into CGRAM with HUD rows 0-1 and dungeon rows
416 // 2-7, matching the software room renderer and vanilla CGRAM layout.
417 auto base_palette = dungeon_main_pal_group[palette_id];
418 std::optional<gfx::SnesPalette> hud_palette_storage;
419 const gfx::SnesPalette* hud_palette = nullptr;
421 hud_palette_storage = game_data_->palette_groups.hud.palette_ref(0);
422 hud_palette = &*hud_palette_storage;
423 }
424 zelda3::LoadDungeonRenderPaletteToCgram(ppu.cgram, base_palette, hud_palette);
425
426 // Load sprite auxiliary palettes (palettes 6-7, indices 90-119)
427 // ROM $0D:D308 = Sprite aux palette group (SNES address, needs LoROM conversion)
428 constexpr uint32_t kSpriteAuxPaletteSnes =
429 0x0DD308; // SNES: bank $0D, addr $D308
430 const uint32_t kSpriteAuxPalettePc =
431 SnesToPc(kSpriteAuxPaletteSnes); // PC: $65308
432 for (int i = 0; i < 30; ++i) {
433 uint32_t addr = kSpriteAuxPalettePc + i * 2;
434 if (addr + 1 < rom_->size()) {
435 uint16_t snes_color = rom_->data()[addr] | (rom_->data()[addr + 1] << 8);
436 ppu.cgram[90 + i] = snes_color;
437 }
438 }
439 printf(
440 "[EMU] Loaded full palette: 90 dungeon + 30 sprite aux = 120 colors\n");
441
442 // 4. Load graphics into VRAM
443 // Graphics buffer contains 8BPP linear data, but VRAM needs 4BPP planar
444 default_room.LoadRoomGraphics();
445 default_room.CopyRoomGraphicsToBuffer();
446 const auto& gfx_buffer = default_room.get_gfx_buffer();
447
448 // Convert 8BPP linear to 4BPP SNES planar format using local function
449 std::vector<uint8_t> linear_data(gfx_buffer.begin(), gfx_buffer.end());
450 auto planar_data = ConvertLinear8bppToPlanar4bpp(linear_data);
451
452 // Copy 4BPP planar data to VRAM (32 bytes = 16 words per tile)
453 for (size_t i = 0; i < planar_data.size() / 2 && i < 0x8000; ++i) {
454 ppu.vram[i] = planar_data[i * 2] | (planar_data[i * 2 + 1] << 8);
455 }
456
457 printf("[EMU] Converted %zu bytes (8BPP linear) to %zu bytes (4BPP planar)\n",
458 gfx_buffer.size(), planar_data.size());
459
460 // 5. CRITICAL: Initialize tilemap buffers in WRAM
461 // Game uses $7E:2000 for BG1 tilemap buffer, $7E:4000 for BG2
462 for (uint32_t i = 0; i < 0x2000; i++) {
463 snes_instance_->Write(0x7E2000 + i, 0x00); // BG1 tilemap buffer
464 snes_instance_->Write(0x7E4000 + i, 0x00); // BG2 tilemap buffer
465 }
466
467 // 5b. CRITICAL: Initialize zero-page tilemap pointers ($BF-$DD)
468 // Handlers use indirect long addressing STA [$BF],Y which requires
469 // 24-bit pointers to be set up. These are NOT stored in ROM - they're
470 // initialized dynamically by the game's room loading code.
471 // We manually set them to point to BG1 tilemap buffer rows.
472 //
473 // BG1 tilemap buffer is at $7E:2000, 64×64 entries (each 2 bytes)
474 // Each row = 64 × 2 = 128 bytes = $80 apart
475 // The 11 pointers at $BF, $C2, $C5... point to different row offsets
476 constexpr uint8_t kPointerZeroPageAddrs[] = {
477 0xBF, 0xC2, 0xC5, 0xC8, 0xCB, 0xCE, 0xD1, 0xD4, 0xD7, 0xDA, 0xDD};
478
479 // Base address for BG1 tilemap in WRAM: $7E2000
480 // Each pointer points to a different row offset for the drawing handlers
481 constexpr uint32_t kBG1TilemapBase = 0x7E2000;
482 constexpr uint32_t kRowStride = 0x80; // 64 tiles × 2 bytes per tile
483
484 for (int i = 0; i < 11; ++i) {
485 uint32_t wram_addr = kBG1TilemapBase + (i * kRowStride);
486 uint8_t lo = wram_addr & 0xFF;
487 uint8_t mid = (wram_addr >> 8) & 0xFF;
488 uint8_t hi = (wram_addr >> 16) & 0xFF;
489
490 uint8_t zp_addr = kPointerZeroPageAddrs[i];
491 // Write 24-bit pointer to direct page in WRAM
492 snes_instance_->Write(0x7E0000 | zp_addr, lo);
493 snes_instance_->Write(0x7E0000 | (zp_addr + 1), mid);
494 snes_instance_->Write(0x7E0000 | (zp_addr + 2), hi);
495
496 printf("[EMU] Tilemap ptr $%02X = $%06X\n", zp_addr, wram_addr);
497 }
498
499 // 6. Setup PPU registers for dungeon rendering
500 snes_instance_->Write(0x002105, 0x09); // BG Mode 1 (4bpp for BG1/2)
501 snes_instance_->Write(0x002107, 0x40); // BG1 tilemap at VRAM $4000 (32x32)
502 snes_instance_->Write(0x002108, 0x48); // BG2 tilemap at VRAM $4800 (32x32)
503 snes_instance_->Write(0x002109, 0x00); // BG1 chr data at VRAM $0000
504 snes_instance_->Write(0x00210A, 0x00); // BG2 chr data at VRAM $0000
505 snes_instance_->Write(0x00212C, 0x03); // Enable BG1+BG2 on main screen
506 snes_instance_->Write(0x002100, 0x0F); // Screen display on, full brightness
507
508 // 6b. CRITICAL: Mock APU I/O registers to prevent infinite handshake loop
509 // The APU handshake at $00:8891 waits for SPC700 to respond with $BBAA
510 // APU has SEPARATE read/write latches:
511 // - Write() goes to in_ports_ (CPU→SPC direction)
512 // - Read() returns from out_ports_ (SPC→CPU direction)
513 // We must set out_ports_ directly for the CPU to see the mock values!
514 auto& apu = snes_instance_->apu();
515 apu.out_ports_[0] = 0xAA; // APU I/O port 0 - ready signal (SPC→CPU)
516 apu.out_ports_[1] = 0xBB; // APU I/O port 1 - ready signal (SPC→CPU)
517 apu.out_ports_[2] = 0x00; // APU I/O port 2
518 apu.out_ports_[3] = 0x00; // APU I/O port 3
519 printf("[EMU] APU mock: out_ports_[0]=$AA, out_ports_[1]=$BB (SPC→CPU)\n");
520
521 // 7. Setup WRAM variables for drawing context
522 snes_instance_->Write(0x7E00AF, room_id_ & 0xFF);
523 snes_instance_->Write(0x7E049C, 0x00);
524 snes_instance_->Write(0x7E049E, 0x00);
525
526 // 7b. Object drawing parameters in zero-page
527 // These are expected by the drawing handlers
528 snes_instance_->Write(0x7E0004, GetObjectType(object_id_)); // Object type
529 uint16_t y_offset = object_y_ * 0x80; // Tilemap Y offset
530 snes_instance_->Write(0x7E0008, y_offset & 0xFF);
531 snes_instance_->Write(0x7E0009, (y_offset >> 8) & 0xFF);
532 snes_instance_->Write(0x7E00B2, object_size_); // Size X parameter
533 snes_instance_->Write(0x7E00B4, object_size_); // Size Y parameter
534
535 // Room state variables
536 snes_instance_->Write(0x7E00A0, room_id_ & 0xFF);
537 snes_instance_->Write(0x7E00A1, (room_id_ >> 8) & 0xFF);
538 printf("[EMU] Object params: type=%d, y_offset=$%04X, size=%d\n",
540
541 // 8. Create object and encode to bytes
543 auto bytes = obj.EncodeObjectToBytes();
544
545 const uint32_t object_data_addr = 0x7E1000;
546 snes_instance_->Write(object_data_addr, bytes.b1);
547 snes_instance_->Write(object_data_addr + 1, bytes.b2);
548 snes_instance_->Write(object_data_addr + 2, bytes.b3);
549 snes_instance_->Write(object_data_addr + 3, 0xFF); // Terminator
550 snes_instance_->Write(object_data_addr + 4, 0xFF);
551
552 // 9. Setup object pointer in WRAM
553 snes_instance_->Write(0x7E00B7, object_data_addr & 0xFF);
554 snes_instance_->Write(0x7E00B8, (object_data_addr >> 8) & 0xFF);
555 snes_instance_->Write(0x7E00B9, (object_data_addr >> 16) & 0xFF);
556
557 // 10. Lookup the object's drawing handler using TWO-TABLE system
558 // Table 1: Data offset table (points into RoomDrawObjectData)
559 // Table 2: Handler routine table (address of drawing routine)
560 // All tables are in bank $01, need LoROM conversion to PC offset
561 auto rom_data = rom_->data();
562 uint32_t data_table_snes = 0;
563 uint32_t handler_table_snes = 0;
564
565 if (object_id_ < 0x100) {
566 // Type 1 objects: $01:8000 (data), $01:8200 (handler)
567 data_table_snes = 0x018000 + (object_id_ * 2);
568 handler_table_snes = 0x018200 + (object_id_ * 2);
569 } else if (object_id_ < 0x200) {
570 // Type 2 objects: $01:8370 (data), $01:8470 (handler)
571 data_table_snes = 0x018370 + ((object_id_ - 0x100) * 2);
572 handler_table_snes = 0x018470 + ((object_id_ - 0x100) * 2);
573 } else {
574 // Type 3 objects: $01:84F0 (data), $01:85F0 (handler)
575 data_table_snes = 0x0184F0 + ((object_id_ - 0x200) * 2);
576 handler_table_snes = 0x0185F0 + ((object_id_ - 0x200) * 2);
577 }
578
579 // Convert SNES addresses to PC offsets for ROM reads
580 uint32_t data_table_pc = SnesToPc(data_table_snes);
581 uint32_t handler_table_pc = SnesToPc(handler_table_snes);
582
583 uint16_t data_offset = 0;
584 uint16_t handler_addr = 0;
585
586 if (data_table_pc + 1 < rom_->size() && handler_table_pc + 1 < rom_->size()) {
587 data_offset = rom_data[data_table_pc] | (rom_data[data_table_pc + 1] << 8);
588 handler_addr =
589 rom_data[handler_table_pc] | (rom_data[handler_table_pc + 1] << 8);
590 } else {
591 last_error_ = "Object ID out of bounds for handler lookup";
592 return;
593 }
594
595 if (handler_addr == 0x0000) {
596 char buf[256];
597 snprintf(buf, sizeof(buf), "Object $%04X has no drawing routine",
598 object_id_);
599 last_error_ = buf;
600 return;
601 }
602
603 printf(
604 "[EMU] Two-table lookup (PC: $%04X, $%04X): data_offset=$%04X, "
605 "handler=$%04X\n",
606 data_table_pc, handler_table_pc, data_offset, handler_addr);
607
608 // 11. Setup CPU state with correct register values
609 cpu.PB = 0x01; // Program bank (handlers in bank $01)
610 cpu.DB = 0x7E; // Data bank (WRAM for tilemap writes)
611 cpu.D = 0x0000; // Direct page at $0000
612 cpu.SetSP(0x01FF); // Stack pointer
613 cpu.status = 0x30; // M=1, X=1 (8-bit A/X/Y mode)
614 cpu.E = 0; // Native 65816 mode, not emulation mode
615
616 // X = data offset (into RoomDrawObjectData at bank $00:9B52)
617 cpu.X = data_offset;
618 // Y = tilemap buffer offset (position in tilemap)
619 cpu.Y = (object_y_ * 0x80) + (object_x_ * 2);
620
621 // 12. Setup return trap with STP instruction
622 // Use STP ($DB) instead of RTL for more reliable handler completion detection
623 // Place STP at $01:FF00 (unused area in bank $01)
624 const uint16_t trap_addr = 0xFF00;
625 snes_instance_->Write(0x01FF00, 0xDB); // STP opcode - stops CPU
626
627 // Push return address for RTL (3 bytes: bank, high, low-1)
628 // RTL adds 1 to the address, so push trap_addr - 1
629 uint16_t sp = cpu.SP();
630 snes_instance_->Write(0x010000 | sp--, 0x01); // Bank byte
631 snes_instance_->Write(0x010000 | sp--, (trap_addr - 1) >> 8); // High
632 snes_instance_->Write(0x010000 | sp--, (trap_addr - 1) & 0xFF); // Low
633 cpu.SetSP(sp);
634
635 // Jump to handler address in bank $01
636 cpu.PC = handler_addr;
637
638 printf("[EMU] Rendering object $%04X at (%d,%d), handler=$%04X\n", object_id_,
639 object_x_, object_y_, handler_addr);
640 printf("[EMU] X=data_offset=$%04X, Y=tilemap_pos=$%04X, PB:PC=$%02X:%04X\n",
641 cpu.X, cpu.Y, cpu.PB, cpu.PC);
642 printf("[EMU] STP trap at $01:%04X for return detection\n", trap_addr);
643
644 // 13. Run emulator with STP detection
645 // Check for STP opcode BEFORE executing to catch the return trap
646 int max_opcodes = 100000;
647 int opcodes = 0;
648 while (opcodes < max_opcodes) {
649 // Check for STP trap - handler has returned
650 uint32_t current_addr = (cpu.PB << 16) | cpu.PC;
651 uint8_t current_opcode = snes_instance_->Read(current_addr);
652 if (current_opcode == 0xDB) {
653 printf("[EMU] STP trap hit at $%02X:%04X - handler completed!\n", cpu.PB,
654 cpu.PC);
655 break;
656 }
657
658 // CRITICAL: Keep refreshing APU out_ports_ to counteract CatchUpApu()
659 // The APU code runs during Read() calls and may overwrite our mock values
660 // Refresh every 100 opcodes to ensure the handshake check passes
661 if ((opcodes & 0x3F) == 0) { // Every 64 opcodes
662 apu.out_ports_[0] = 0xAA;
663 apu.out_ports_[1] = 0xBB;
664 }
665
666 // Detect APU handshake loop at $00:8891 and force skip it
667 // The loop reads $2140, compares to $AA, branches if not equal
668 if (cpu.PB == 0x00 && cpu.PC == 0x8891) {
669 // We're stuck in APU handshake - this shouldn't happen with the mock
670 // but if it does, force the check to pass by setting accumulator
671 static int apu_loop_count = 0;
672 if (++apu_loop_count > 100) {
673 printf("[EMU] WARNING: Stuck in APU loop at $00:8891, forcing skip\n");
674 // Skip past the loop by advancing PC (typical pattern is ~6 bytes)
675 cpu.PC = 0x8898; // Approximate address after the handshake loop
676 apu_loop_count = 0;
677 }
678 }
679
680 cpu.RunOpcode();
681 opcodes++;
682
683 // Debug: Sample WRAM after 10k opcodes to see if handler is writing
684 if (opcodes == 10000) {
685 printf("[EMU] WRAM $7E2000 after 10k opcodes: ");
686 for (int i = 0; i < 8; i++) {
687 printf("%04X ", snes_instance_->Read(0x7E2000 + i * 2) |
688 (snes_instance_->Read(0x7E2001 + i * 2) << 8));
689 }
690 printf("\n");
691 }
692 }
693
694 last_cycle_count_ = opcodes;
695
696 printf("[EMU] Completed after %d opcodes, PC=$%02X:%04X\n", opcodes, cpu.PB,
697 cpu.PC);
698
699 if (opcodes >= max_opcodes) {
700 last_error_ = "Timeout: exceeded max cycles";
701 // Debug: Print some WRAM tilemap values to see if anything was written
702 printf("[EMU] WRAM BG1 tilemap sample at $7E2000:\n");
703 for (int i = 0; i < 16; i++) {
704 printf(" %04X", snes_instance_->Read(0x7E2000 + i * 2) |
705 (snes_instance_->Read(0x7E2000 + i * 2 + 1) << 8));
706 }
707 printf("\n");
708 // Handler didn't complete - PPU state may be corrupted, skip rendering
709 // Reset SNES to clean state to prevent crash on destruction
710 snes_instance_->Reset(true);
711 return;
712 }
713
714 // 14. Copy WRAM tilemap buffers to VRAM
715 // Game drawing routines write to WRAM, but PPU reads from VRAM
716 // BG1: WRAM $7E2000 → VRAM $4000 (2KB = 32x32 tilemap)
717 for (uint32_t i = 0; i < 0x800; i++) {
718 uint8_t lo = snes_instance_->Read(0x7E2000 + i * 2);
719 uint8_t hi = snes_instance_->Read(0x7E2000 + i * 2 + 1);
720 ppu.vram[0x4000 + i] = lo | (hi << 8);
721 }
722 // BG2: WRAM $7E4000 → VRAM $4800 (2KB = 32x32 tilemap)
723 for (uint32_t i = 0; i < 0x800; i++) {
724 uint8_t lo = snes_instance_->Read(0x7E4000 + i * 2);
725 uint8_t hi = snes_instance_->Read(0x7E4000 + i * 2 + 1);
726 ppu.vram[0x4800 + i] = lo | (hi << 8);
727 }
728
729 // Debug: Print VRAM tilemap sample to verify data was copied
730 printf("[EMU] VRAM tilemap at $4000 (BG1): ");
731 for (int i = 0; i < 8; i++) {
732 printf("%04X ", ppu.vram[0x4000 + i]);
733 }
734 printf("\n");
735
736 // 15. Force PPU to render the tilemaps
737 ppu.HandleFrameStart();
738 for (int line = 0; line < 224; line++) {
739 ppu.RunLine(line);
740 }
741 ppu.HandleVblank();
742
743 // 15. Get the rendered pixels from PPU
744 void* pixels = nullptr;
745 int pitch = 0;
746 if (renderer_->LockTexture(object_texture_, nullptr, &pixels, &pitch)) {
747 snes_instance_->SetPixels(static_cast<uint8_t*>(pixels));
749 }
750}
751
753 if (!rom_ || !rom_->is_loaded()) {
754 last_error_ = "ROM not loaded";
755 return;
756 }
757
758 last_error_.clear();
759
760 // Use shared render service if available
764 request.entity_id = object_id_;
765 request.x = object_x_;
766 request.y = object_y_;
767 request.size = object_size_;
768 request.room_id = room_id_;
769 request.output_width = 256;
770 request.output_height = 256;
771
772 auto result = render_service_->Render(request);
773 if (result.ok() && result->success) {
774 // Update texture with rendered pixels
775 if (!object_texture_) {
777 }
778 void* pixels = nullptr;
779 int pitch = 0;
780 if (renderer_->LockTexture(object_texture_, nullptr, &pixels, &pitch)) {
781 // Copy RGBA pixels to texture
782 memcpy(pixels, result->rgba_pixels.data(), result->rgba_pixels.size());
784 }
785 printf("[SERVICE] Rendered object $%04X via EmulatorRenderService\n",
786 object_id_);
787 return;
788 } else {
789 // Fall through to legacy rendering
790 printf("[SERVICE] Render failed, falling back to legacy: %s\n",
791 result.ok() ? result->error.c_str()
792 : std::string(result.status().message()).c_str());
793 }
794 }
795
796 // Legacy rendering path (when no render service is available)
797 // Load room for palette/graphics context
799 room.SetGameData(game_data_); // Ensure room has access to GameData
800
801 // Get dungeon main palette (palettes 0-5, 90 colors)
802 if (!game_data_) {
803 last_error_ = "GameData not available";
804 return;
805 }
806 auto dungeon_main_pal_group = game_data_->palette_groups.dungeon_main;
807 int palette_id = room.palette();
808 if (palette_id < 0 ||
809 palette_id >= static_cast<int>(dungeon_main_pal_group.size())) {
810 palette_id = 0;
811 }
812 auto base_palette = dungeon_main_pal_group[palette_id];
813
814 // Build full palette including sprite auxiliary palettes (6-7)
815 // Dungeon main: palettes 0-5 (90 colors)
816 // Sprite aux: palettes 6-7 (30 colors) from ROM
817 gfx::SnesPalette palette;
818
819 // Copy dungeon main palette (0-89)
820 for (size_t i = 0; i < base_palette.size() && i < 90; ++i) {
821 palette.AddColor(base_palette[i]);
822 }
823 // Pad to 90 if needed
824 while (palette.size() < 90) {
825 palette.AddColor(gfx::SnesColor(0));
826 }
827
828 // Load sprite auxiliary palettes (90-119) from ROM $0D:D308
829 // These are palettes 6-7 used by some dungeon tiles
830 // SNES address needs LoROM conversion to PC offset
831 constexpr uint32_t kSpriteAuxPaletteSnes =
832 0x0DD308; // SNES: bank $0D, addr $D308
833 const uint32_t kSpriteAuxPalettePc =
834 SnesToPc(kSpriteAuxPaletteSnes); // PC: $65308
835 for (int i = 0; i < 30; ++i) {
836 uint32_t addr = kSpriteAuxPalettePc + i * 2;
837 if (addr + 1 < rom_->size()) {
838 uint16_t snes_color = rom_->data()[addr] | (rom_->data()[addr + 1] << 8);
840 } else {
841 palette.AddColor(gfx::SnesColor(0));
842 }
843 }
844
845 // Load room graphics
846 room.LoadRoomGraphics();
848 const auto& gfx_buffer = room.get_gfx_buffer();
849
850 // Create ObjectDrawer with the room's graphics buffer
852 std::make_unique<zelda3::ObjectDrawer>(rom_, room_id_, gfx_buffer.data());
853 object_drawer_->InitializeDrawRoutines();
854
855 // Clear background buffers (default 512x512)
858
859 // Initialize the internal bitmaps for drawing
860 // BackgroundBuffer's bitmap needs to be created before ObjectDrawer can draw
861 constexpr int kBgSize = 512; // Default BackgroundBuffer size
862 preview_bg1_.bitmap().Create(kBgSize, kBgSize, 8,
863 std::vector<uint8_t>(kBgSize * kBgSize, 0));
864 preview_bg2_.bitmap().Create(kBgSize, kBgSize, 8,
865 std::vector<uint8_t>(kBgSize * kBgSize, 0));
866
867 // Create RoomObject and draw it using ObjectDrawer
869
870 // Create palette group for drawing
871 gfx::PaletteGroup preview_palette_group;
872 preview_palette_group.AddPalette(palette);
873
874 // Draw the object
875 auto status = object_drawer_->DrawObject(obj, preview_bg1_, preview_bg2_,
876 preview_palette_group);
877 if (!status.ok()) {
878 last_error_ = std::string(status.message());
879 printf("[STATIC] DrawObject failed: %s\n", last_error_.c_str());
880 return;
881 }
882
883 printf("[STATIC] Drew object $%04X at (%d,%d) size=%d\n", object_id_,
885
886 // Get the rendered bitmap data from the BackgroundBuffer
887 auto& bg1_bitmap = preview_bg1_.bitmap();
888 auto& bg2_bitmap = preview_bg2_.bitmap();
889
890 // Create preview bitmap if needed (use 256x256 for display)
891 // Use 0xFF as "unwritten/transparent" marker since 0 is a valid palette index
892 constexpr int kPreviewSize = 256;
893 constexpr uint8_t kTransparentMarker = 0xFF;
894 if (preview_bitmap_.width() != kPreviewSize) {
896 kPreviewSize, kPreviewSize, 8,
897 std::vector<uint8_t>(kPreviewSize * kPreviewSize, kTransparentMarker));
898 } else {
899 // Clear to transparent marker
900 std::fill(preview_bitmap_.mutable_data().begin(),
901 preview_bitmap_.mutable_data().end(), kTransparentMarker);
902 }
903
904 // Copy center portion of 512x512 buffer to 256x256 preview
905 // This shows the object which is typically placed near center
906 auto& preview_data = preview_bitmap_.mutable_data();
907 const auto& bg1_data = bg1_bitmap.vector();
908 const auto& bg2_data = bg2_bitmap.vector();
909
910 // Calculate offset to center on object position
911 int offset_x = std::max(0, (object_x_ * 8) - kPreviewSize / 2);
912 int offset_y = std::max(0, (object_y_ * 8) - kPreviewSize / 2);
913
914 // Clamp to stay within bounds
915 offset_x = std::min(offset_x, kBgSize - kPreviewSize);
916 offset_y = std::min(offset_y, kBgSize - kPreviewSize);
917
918 // Composite: first BG2, then BG1 on top
919 // Note: BG buffers use 0 for transparent/unwritten pixels
920 for (int y = 0; y < kPreviewSize; ++y) {
921 for (int x = 0; x < kPreviewSize; ++x) {
922 size_t src_idx = (offset_y + y) * kBgSize + (offset_x + x);
923 int dst_idx = y * kPreviewSize + x;
924
925 // BG2 first (background layer)
926 // Source uses 0 for transparent, but 0 can also be a valid palette index
927 // We need to check if the pixel was actually drawn (non-zero in source)
928 if (src_idx < bg2_data.size() && bg2_data[src_idx] != 0) {
929 preview_data[dst_idx] = bg2_data[src_idx];
930 }
931 // BG1 on top (foreground layer)
932 if (src_idx < bg1_data.size() && bg1_data[src_idx] != 0) {
933 preview_data[dst_idx] = bg1_data[src_idx];
934 }
935 }
936 }
937
938 // Create/update texture
939 if (!object_texture_ && renderer_) {
940 object_texture_ = renderer_->CreateTexture(kPreviewSize, kPreviewSize);
941 }
942
943 if (object_texture_ && renderer_) {
944 // Convert indexed bitmap to RGBA for texture
945 std::vector<uint8_t> rgba_data(kPreviewSize * kPreviewSize * 4);
946 for (int y = 0; y < kPreviewSize; ++y) {
947 for (int x = 0; x < kPreviewSize; ++x) {
948 size_t idx = y * kPreviewSize + x;
949 uint8_t color_idx = preview_data[idx];
950
951 if (color_idx == kTransparentMarker) {
952 // Unwritten pixel - show background
953 rgba_data[idx * 4 + 0] = 32;
954 rgba_data[idx * 4 + 1] = 32;
955 rgba_data[idx * 4 + 2] = 48;
956 rgba_data[idx * 4 + 3] = 255;
957 } else if (color_idx < palette.size()) {
958 // Valid palette index - look up color (now supports 0-119)
959 auto color = palette[color_idx];
960 rgba_data[idx * 4 + 0] = color.rgb().x; // R
961 rgba_data[idx * 4 + 1] = color.rgb().y; // G
962 rgba_data[idx * 4 + 2] = color.rgb().z; // B
963 rgba_data[idx * 4 + 3] = 255; // A
964 } else {
965 // Out-of-bounds palette index (>119)
966 // Show as magenta to indicate error
967 rgba_data[idx * 4 + 0] = 255;
968 rgba_data[idx * 4 + 1] = 0;
969 rgba_data[idx * 4 + 2] = 255;
970 rgba_data[idx * 4 + 3] = 255;
971 }
972 }
973 }
974
975 void* pixels = nullptr;
976 int pitch = 0;
977 if (renderer_->LockTexture(object_texture_, nullptr, &pixels, &pitch)) {
978 memcpy(pixels, rgba_data.data(), rgba_data.size());
980 }
981 }
982
983 static_render_dirty_ = false;
984 printf("[STATIC] Render complete\n");
985}
986
988 if (id < 0)
989 return "Invalid";
990
991 if (id < 0x100) {
992 // Type 1 objects (0x00-0xFF)
993 if (id < static_cast<int>(std::size(zelda3::Type1RoomObjectNames))) {
994 return zelda3::Type1RoomObjectNames[id];
995 }
996 } else if (id < 0x200) {
997 // Type 2 objects (0x100-0x1FF)
998 int index = id - 0x100;
999 if (index < static_cast<int>(std::size(zelda3::Type2RoomObjectNames))) {
1000 return zelda3::Type2RoomObjectNames[index];
1001 }
1002 } else if (id < 0x300) {
1003 // Type 3 objects (0x200-0x2FF)
1004 int index = id - 0x200;
1005 if (index < static_cast<int>(std::size(zelda3::Type3RoomObjectNames))) {
1006 return zelda3::Type3RoomObjectNames[index];
1007 }
1008 }
1009
1010 return "Unknown Object";
1011}
1012
1014 if (id < 0x100)
1015 return 1;
1016 if (id < 0x200)
1017 return 2;
1018 if (id < 0x300)
1019 return 3;
1020 return 0;
1021}
1022
1024 const auto& theme = AgentUI::GetTheme();
1025
1027 ImGui::BeginChild("StatusPanel", ImVec2(0, 100), true);
1028
1029 ImGui::TextColored(theme.text_info, tr("Execution Status"));
1030 ImGui::Separator();
1031
1032 // Cycle count with status color
1033 ImGui::Text(tr("Cycles:"));
1034 ImGui::SameLine();
1035 if (last_cycle_count_ >= 100000) {
1036 ImGui::TextColored(theme.status_error, tr("%d (TIMEOUT)"),
1038 } else if (last_cycle_count_ > 0) {
1039 ImGui::TextColored(theme.status_success, "%d", last_cycle_count_);
1040 } else {
1041 ImGui::TextColored(theme.text_secondary_gray, tr("Not yet executed"));
1042 }
1043
1044 // Error status
1045 ImGui::Text(tr("Status:"));
1046 ImGui::SameLine();
1047 if (last_error_.empty()) {
1048 if (last_cycle_count_ > 0) {
1049 ImGui::TextColored(theme.status_success, tr("OK"));
1050 } else {
1051 ImGui::TextColored(theme.text_secondary_gray, tr("Ready"));
1052 }
1053 } else {
1054 ImGui::TextColored(theme.status_error, "%s", last_error_.c_str());
1055 }
1056
1057 ImGui::EndChild();
1059}
1060
1062 const auto& theme = AgentUI::GetTheme();
1063
1064 ImGui::SetNextWindowSize(ImVec2(600, 500), ImGuiCond_FirstUseEver);
1065 if (ImGui::Begin("Object Browser", &show_browser_)) {
1066 ImGui::TextColored(theme.text_info,
1067 tr("Browse all dungeon objects by type and category"));
1068 ImGui::Separator();
1069
1070 if (ImGui::BeginTabBar("ObjectTypeTabs")) {
1071 // Type 1 objects tab
1072 if (ImGui::BeginTabItem(tr("Type 1 (0x00-0xFF)"))) {
1073 ImGui::TextDisabled(tr("Walls, floors, and common dungeon elements"));
1074 ImGui::Separator();
1075
1076 ImGui::BeginChild("Type1List", ImVec2(0, 0), false);
1077 for (int i = 0;
1078 i < static_cast<int>(std::size(zelda3::Type1RoomObjectNames));
1079 ++i) {
1080 char label[256];
1081 snprintf(label, sizeof(label), "0x%02X: %s", i,
1082 zelda3::Type1RoomObjectNames[i]);
1083
1084 if (ImGui::Selectable(label, object_id_ == i)) {
1085 object_id_ = i;
1086 show_browser_ = false;
1089 } else {
1091 }
1092 }
1093 }
1094 ImGui::EndChild();
1095
1096 ImGui::EndTabItem();
1097 }
1098
1099 // Type 2 objects tab
1100 if (ImGui::BeginTabItem(tr("Type 2 (0x100-0x1FF)"))) {
1101 ImGui::TextDisabled(tr("Corners, furniture, and special objects"));
1102 ImGui::Separator();
1103
1104 ImGui::BeginChild("Type2List", ImVec2(0, 0), false);
1105 for (int i = 0;
1106 i < static_cast<int>(std::size(zelda3::Type2RoomObjectNames));
1107 ++i) {
1108 char label[256];
1109 int id = 0x100 + i;
1110 snprintf(label, sizeof(label), "0x%03X: %s", id,
1111 zelda3::Type2RoomObjectNames[i]);
1112
1113 if (ImGui::Selectable(label, object_id_ == id)) {
1114 object_id_ = id;
1115 show_browser_ = false;
1118 } else {
1120 }
1121 }
1122 }
1123 ImGui::EndChild();
1124
1125 ImGui::EndTabItem();
1126 }
1127
1128 // Type 3 objects tab
1129 if (ImGui::BeginTabItem(tr("Type 3 (0x200-0x2FF)"))) {
1130 ImGui::TextDisabled(
1131 tr("Interactive objects, chests, and special items"));
1132 ImGui::Separator();
1133
1134 ImGui::BeginChild("Type3List", ImVec2(0, 0), false);
1135 for (int i = 0;
1136 i < static_cast<int>(std::size(zelda3::Type3RoomObjectNames));
1137 ++i) {
1138 char label[256];
1139 int id = 0x200 + i;
1140 snprintf(label, sizeof(label), "0x%03X: %s", id,
1141 zelda3::Type3RoomObjectNames[i]);
1142
1143 if (ImGui::Selectable(label, object_id_ == id)) {
1144 object_id_ = id;
1145 show_browser_ = false;
1148 } else {
1150 }
1151 }
1152 }
1153 ImGui::EndChild();
1154
1155 ImGui::EndTabItem();
1156 }
1157
1158 ImGui::EndTabBar();
1159 }
1160 }
1161 ImGui::End();
1162}
1163
1164} // namespace gui
1165} // 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 > Render(const RenderRequest &request)
void Create(int width, int height, int depth, std::span< uint8_t > data)
Create a bitmap with the given dimensions and data.
Definition bitmap.cc:201
int width() const
Definition bitmap.h:394
std::vector< uint8_t > & mutable_data()
Definition bitmap.h:399
Defines an abstract interface for all rendering operations.
Definition irenderer.h:60
virtual void UnlockTexture(TextureHandle texture)=0
virtual TextureHandle CreateTexture(int width, int height)=0
Creates a new, empty texture.
virtual bool LockTexture(TextureHandle texture, SDL_Rect *rect, void **pixels, int *pitch)=0
SNES Color container.
Definition snes_color.h:110
Represents a palette of colors for the Super Nintendo Entertainment System (SNES).
void AddColor(const SnesColor &color)
RAII scope that enables automatic widget registration.
std::unique_ptr< zelda3::ObjectDrawer > object_drawer_
emu::render::EmulatorRenderService * render_service_
void Initialize(gfx::IRenderer *renderer, Rom *rom, zelda3::GameData *game_data=nullptr, emu::render::EmulatorRenderService *render_service=nullptr)
ObjectBytes EncodeObjectToBytes() const
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
std::vector< uint8_t > ConvertLinear8bppToPlanar4bpp(const std::vector< uint8_t > &linear_data)
bool StyledButton(const char *label, const ImVec4 &color, const ImVec2 &size)
const AgentUITheme & GetTheme()
void VerticalSpacing(float amount)
Editors are the view controllers for the application.
bool AutoInputInt(const char *label, int *v, int step=1, int step_fast=100, ImGuiInputTextFlags flags=0)
bool AutoSliderInt(const char *label, int *v, int v_min, int v_max, const char *format="%d", ImGuiSliderFlags flags=0)
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
uint32_t SnesToPc(uint32_t addr) noexcept
Definition snes.h:8
SNES color in 15-bit RGB format (BGR555)
Represents a group of palettes.
const SnesPalette & palette_ref(int i) const
void AddPalette(SnesPalette pal)
gfx::PaletteGroupMap palette_groups
Definition game_data.h:92
Automatic widget registration helpers for ImGui Test Engine integration.
struct snes_color snes_color
SNES color in 15-bit RGB format (BGR555)