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