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