diff --git a/src/app/editor/dungeon/dungeon_canvas_viewer.cc b/src/app/editor/dungeon/dungeon_canvas_viewer.cc index 300b05da..f178c9a1 100644 --- a/src/app/editor/dungeon/dungeon_canvas_viewer.cc +++ b/src/app/editor/dungeon/dungeon_canvas_viewer.cc @@ -7,6 +7,7 @@ #include "app/gui/canvas.h" #include "app/gui/input.h" #include "app/rom.h" +#include "app/zelda3/dungeon/object_drawer.h" #include "app/zelda3/dungeon/object_renderer.h" #include "app/zelda3/dungeon/room.h" #include "app/zelda3/sprite/sprite.h" @@ -111,10 +112,13 @@ void DungeonCanvasViewer::DrawDungeonCanvas(int room_id) { room.LoadObjects(); } - // Render background layers with proper positioning + // NOTE: Don't draw objects here - RenderRoomBackgroundLayers() already does it + // via room.RenderRoomGraphics() which calls RenderObjectsToBackground() + + // Render background layers from arena buffers RenderRoomBackgroundLayers(room_id); - // Render room objects with proper graphics + // Render room objects with proper graphics (old system as fallback) if (current_palette_id_ < current_palette_group_.size()) { auto room_palette = current_palette_group_[current_palette_id_]; @@ -652,31 +656,45 @@ absl::Status DungeonCanvasViewer::UpdateRoomBackgroundLayers(int room_id) { void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { if (room_id < 0 || room_id >= 128) { + printf("[Canvas] Invalid room_id: %d\n", room_id); return; } if (!rom_ || !rom_->is_loaded()) { + printf("[Canvas] ROM not loaded\n"); return; } if (!rooms_) { + printf("[Canvas] Rooms pointer is null\n"); return; } - // Get canvas dimensions to limit rendering + // Get canvas dimensions int canvas_width = canvas_.width(); int canvas_height = canvas_.height(); - // Validate canvas dimensions + printf("[Canvas] Canvas size: %dx%d\n", canvas_width, canvas_height); + if (canvas_width <= 0 || canvas_height <= 0) { + printf("[Canvas] Invalid canvas dimensions\n"); return; } - // Render the room's background layers using the graphics arena - // BG1 (background layer 1) - main room graphics + // Render BG1 (background layer 1) - main room graphics auto& bg1_bitmap = gfx::Arena::Get().bg1().bitmap(); + printf("[Canvas] BG1: active=%d, size=%dx%d, texture=%p\n", + bg1_bitmap.is_active(), bg1_bitmap.width(), bg1_bitmap.height(), + (void*)bg1_bitmap.texture()); + if (bg1_bitmap.is_active() && bg1_bitmap.width() > 0 && bg1_bitmap.height() > 0) { - // Scale the background to fit the canvas + // Ensure texture exists + if (!bg1_bitmap.texture()) { + printf("[Canvas] WARNING: BG1 has no texture, creating...\n"); + core::Renderer::Get().RenderBitmap(&bg1_bitmap); + } + + // Scale to fit canvas float scale_x = static_cast(canvas_width) / bg1_bitmap.width(); float scale_y = static_cast(canvas_height) / bg1_bitmap.height(); float scale = std::min(scale_x, scale_y); @@ -686,13 +704,21 @@ void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { int offset_x = (canvas_width - scaled_width) / 2; int offset_y = (canvas_height - scaled_height) / 2; + printf("[Canvas] Drawing BG1 at offset=(%d,%d), scaled_size=%dx%d, scale=%.2f\n", + offset_x, offset_y, scaled_width, scaled_height, scale); + canvas_.DrawBitmap(bg1_bitmap, offset_x, offset_y, scale, 255); + } else { + printf("[Canvas] BG1 not ready for rendering\n"); } - // BG2 (background layer 2) - sprite graphics (overlay) + // Render BG2 (background layer 2) - sprite graphics (overlay) auto& bg2_bitmap = gfx::Arena::Get().bg2().bitmap(); if (bg2_bitmap.is_active() && bg2_bitmap.width() > 0 && bg2_bitmap.height() > 0) { - // Scale the background to fit the canvas + if (!bg2_bitmap.texture()) { + core::Renderer::Get().RenderBitmap(&bg2_bitmap); + } + float scale_x = static_cast(canvas_width) / bg2_bitmap.width(); float scale_y = static_cast(canvas_height) / bg2_bitmap.height(); float scale = std::min(scale_x, scale_y); @@ -702,8 +728,13 @@ void DungeonCanvasViewer::RenderRoomBackgroundLayers(int room_id) { int offset_x = (canvas_width - scaled_width) / 2; int offset_y = (canvas_height - scaled_height) / 2; - canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200); // Semi-transparent overlay + printf("[Canvas] Drawing BG2 at offset=(%d,%d), scaled_size=%dx%d, scale=%.2f\n", + offset_x, offset_y, scaled_width, scaled_height, scale); + + canvas_.DrawBitmap(bg2_bitmap, offset_x, offset_y, scale, 200); } + + printf("[Canvas] RenderRoomBackgroundLayers complete\n"); } } // namespace yaze::editor diff --git a/src/app/editor/dungeon/dungeon_editor_v2.cc b/src/app/editor/dungeon/dungeon_editor_v2.cc index a0ba5f10..4966e095 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.cc +++ b/src/app/editor/dungeon/dungeon_editor_v2.cc @@ -11,15 +11,11 @@ namespace yaze::editor { -using ImGui::BeginTable; -using ImGui::EndTable; -using ImGui::TableHeadersRow; -using ImGui::TableNextColumn; -using ImGui::TableNextRow; -using ImGui::TableSetupColumn; +// No table layout needed - all cards are independent void DungeonEditorV2::Initialize() { - // No complex initialization needed - components handle themselves + // Don't initialize emulator preview yet - ROM might not be loaded + // Will be initialized in Load() instead } absl::Status DungeonEditorV2::Load() { @@ -52,20 +48,35 @@ absl::Status DungeonEditorV2::Load() { object_selector_.SetCurrentPaletteId(current_palette_id_); object_selector_.set_rooms(&rooms_); + // NOW initialize emulator preview with loaded ROM + object_emulator_preview_.Initialize(rom_); + is_loaded_ = true; return absl::OkStatus(); } absl::Status DungeonEditorV2::Update() { if (!is_loaded_) { - ImGui::Text("Loading..."); + // Show minimal loading message in parent window + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), "Dungeon Editor Loading..."); + ImGui::TextWrapped("Independent editor cards will appear once ROM data is loaded."); return absl::OkStatus(); } + // Minimize parent window content - just show a toolbar DrawToolset(); - gui::VerticalSpacing(2.0f); + + ImGui::Separator(); + ImGui::TextColored(ImVec4(0.5f, 0.5f, 0.5f, 1.0f), + "Editor cards are independent windows - dock them anywhere!"); + ImGui::TextWrapped( + "Room Selector, Object Selector, and Room cards can be freely arranged. " + "This parent window can be minimized or closed."); + // Render all independent cards (these create their own top-level windows) + object_emulator_preview_.Render(); DrawLayout(); + return absl::OkStatus(); } @@ -98,32 +109,33 @@ void DungeonEditorV2::DrawToolset() { } void DungeonEditorV2::DrawLayout() { - // Simple 3-column layout as designed - if (BeginTable("##DungeonEditTable", 3, - ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV, - ImVec2(0, 0))) { - TableSetupColumn("Room Selector", ImGuiTableColumnFlags_WidthFixed, 250); - TableSetupColumn("Canvas", ImGuiTableColumnFlags_WidthStretch); - TableSetupColumn("Object Selector", ImGuiTableColumnFlags_WidthFixed, 300); - TableHeadersRow(); - TableNextRow(); - - // Column 1: Room Selector (fully delegated) - TableNextColumn(); - room_selector_.Draw(); - - // Column 2: Canvas area for active room cards - TableNextColumn(); - // This column is now just a docking space. The cards themselves are independent windows. - - // Column 3: Object Selector (fully delegated) - TableNextColumn(); - object_selector_.Draw(); - - EndTable(); + // NO TABLE LAYOUT - All independent dockable EditorCards + + // 1. Room Selector Card (independent, dockable) + { + static bool show_room_selector = true; + gui::EditorCard selector_card( + MakeCardTitle("Room Selector").c_str(), + ICON_MD_LIST, &show_room_selector); + if (selector_card.Begin()) { + room_selector_.Draw(); + } + selector_card.End(); } - // Draw active rooms as individual, dockable EditorCards + // 2. Object Selector/Manager Card (independent, dockable) + { + static bool show_object_selector = true; + gui::EditorCard object_card( + MakeCardTitle("Object Selector").c_str(), + ICON_MD_CATEGORY, &show_object_selector); + if (object_card.Begin()) { + object_selector_.Draw(); + } + object_card.End(); + } + + // 3. Active Room Cards (independent, dockable, no inheritance) for (int i = 0; i < active_rooms_.Size; i++) { int room_id = active_rooms_[i]; bool open = true; @@ -138,13 +150,13 @@ void DungeonEditorV2::DrawLayout() { std::string card_name_str = absl::StrFormat("%s###RoomCard%d", MakeCardTitle(base_name).c_str(), room_id); - const char* card_name = card_name_str.c_str(); - gui::EditorCard room_card(card_name, ICON_MD_GRID_ON, &open); + // Each room card is COMPLETELY independent - no parent windows + gui::EditorCard room_card(card_name_str.c_str(), ICON_MD_GRID_ON, &open); if (room_card.Begin()) { DrawRoomTab(room_id); } - room_card.End(); // ALWAYS call End after Begin + room_card.End(); if (!open) { active_rooms_.erase(active_rooms_.Data + i); diff --git a/src/app/editor/dungeon/dungeon_editor_v2.h b/src/app/editor/dungeon/dungeon_editor_v2.h index bcaba475..af11ff76 100644 --- a/src/app/editor/dungeon/dungeon_editor_v2.h +++ b/src/app/editor/dungeon/dungeon_editor_v2.h @@ -13,6 +13,7 @@ #include "app/zelda3/dungeon/room.h" #include "app/zelda3/dungeon/room_entrance.h" #include "app/gui/editor_layout.h" +#include "app/gui/widgets/dungeon_object_emulator_preview.h" #include "imgui/imgui.h" namespace yaze { @@ -39,7 +40,8 @@ class DungeonEditorV2 : public Editor { room_loader_(rom), room_selector_(rom), canvas_viewer_(rom), - object_selector_(rom) { + object_selector_(rom), + object_emulator_preview_() { type_ = EditorType::kDungeon; } @@ -62,6 +64,7 @@ class DungeonEditorV2 : public Editor { room_selector_.set_rom(rom); canvas_viewer_.SetRom(rom); object_selector_.SetRom(rom); + object_emulator_preview_.Initialize(rom); } Rom* rom() const { return rom_; } @@ -105,6 +108,7 @@ class DungeonEditorV2 : public Editor { DungeonRoomSelector room_selector_; DungeonCanvasViewer canvas_viewer_; DungeonObjectSelector object_selector_; + gui::DungeonObjectEmulatorPreview object_emulator_preview_; bool is_loaded_ = false; }; diff --git a/src/app/emu/cpu/cpu.h b/src/app/emu/cpu/cpu.h index 7d24857d..7dfd9e2b 100644 --- a/src/app/emu/cpu/cpu.h +++ b/src/app/emu/cpu/cpu.h @@ -767,6 +767,7 @@ class Cpu { } auto mutable_log_instructions() -> bool* { return &log_instructions_; } + bool stopped() const { return stopped_; } private: void compare(uint16_t register_value, uint16_t memory_value) { diff --git a/src/app/emu/video/ppu.h b/src/app/emu/video/ppu.h index ee76ab76..24bdafea 100644 --- a/src/app/emu/video/ppu.h +++ b/src/app/emu/video/ppu.h @@ -321,7 +321,6 @@ class Ppu { // Set pixel output format (0 = BGRX, 1 = XBGR) void SetPixelFormat(uint8_t format) { pixelOutputFormat = format; } - private: int GetPixelForMode7(int x, int layer, bool priority); const int cyclesPerScanline = 341; // SNES PPU has 341 cycles per scanline @@ -343,6 +342,9 @@ class Ppu { // cgram access uint16_t cgram[0x100]; + + private: + uint8_t cgram_pointer_; bool cgram_second_write_; uint8_t cgram_buffer_; diff --git a/src/app/gui/gui_library.cmake b/src/app/gui/gui_library.cmake index f3ba0313..b0fdf0cb 100644 --- a/src/app/gui/gui_library.cmake +++ b/src/app/gui/gui_library.cmake @@ -3,6 +3,7 @@ set( app/gui/modules/asset_browser.cc app/gui/modules/text_editor.cc app/gui/widgets/agent_chat_widget.cc + app/gui/widgets/dungeon_object_emulator_preview.cc app/gui/widgets/collaboration_panel.cc app/gui/canvas.cc app/gui/canvas_utils.cc diff --git a/src/app/gui/widgets/dungeon_object_emulator_preview.cc b/src/app/gui/widgets/dungeon_object_emulator_preview.cc new file mode 100644 index 00000000..a3f9fcb6 --- /dev/null +++ b/src/app/gui/widgets/dungeon_object_emulator_preview.cc @@ -0,0 +1,296 @@ +#include "app/gui/widgets/dungeon_object_emulator_preview.h" + +#include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/room_object.h" +#include "app/gui/widgets/widget_auto_register.h" +#include "app/core/window.h" +#include + +namespace yaze { +namespace gui { + +DungeonObjectEmulatorPreview::DungeonObjectEmulatorPreview() { + snes_instance_ = std::make_unique(); + object_texture_ = SDL_CreateTexture(core::Renderer::Get().renderer(), + SDL_PIXELFORMAT_ARGB8888, + SDL_TEXTUREACCESS_STREAMING, 256, 256); +} + +DungeonObjectEmulatorPreview::~DungeonObjectEmulatorPreview() { + if (object_texture_) { + SDL_DestroyTexture(object_texture_); + } +} + +void DungeonObjectEmulatorPreview::Initialize(Rom* rom) { + rom_ = rom; + if (rom_ && rom_->is_loaded()) { + auto rom_data = rom_->vector(); + snes_instance_->Init(rom_data); + } +} + +void DungeonObjectEmulatorPreview::Render() { + if (!show_window_) return; + + if (ImGui::Begin("Dungeon Object Emulator Preview", &show_window_, ImGuiWindowFlags_AlwaysAutoResize)) { + AutoWidgetScope scope("DungeonEditor/EmulatorPreview"); + + // ROM status indicator + if (rom_ && rom_->is_loaded()) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "ROM: Loaded ✓"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "ROM: Not loaded ✗"); + } + + ImGui::Separator(); + RenderControls(); + ImGui::Separator(); + + // Preview image with border + if (object_texture_) { + ImGui::BeginChild("PreviewRegion", ImVec2(260, 260), true, ImGuiWindowFlags_NoScrollbar); + ImGui::Image((ImTextureID)object_texture_, ImVec2(256, 256)); + ImGui::EndChild(); + } else { + ImGui::TextColored(ImVec4(1.0f, 1.0f, 0.0f, 1.0f), "No texture available"); + } + + // Debug info section + ImGui::Separator(); + ImGui::Text("Execution:"); + ImGui::Indent(); + ImGui::Text("Cycles: %d %s", last_cycle_count_, + last_cycle_count_ >= 100000 ? "(TIMEOUT)" : ""); + ImGui::Unindent(); + + // Status with color coding + ImGui::Text("Status:"); + ImGui::Indent(); + if (last_error_.empty()) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "✓ OK"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "✗ %s", last_error_.c_str()); + } + ImGui::Unindent(); + + // Help text + ImGui::Separator(); + ImGui::TextWrapped("This tool uses the SNES emulator to render objects by " + "executing the game's native drawing routines from bank $01."); + } + ImGui::End(); +} + +void DungeonObjectEmulatorPreview::RenderControls() { + ImGui::Text("Object Configuration:"); + ImGui::Indent(); + + // Object ID with hex display + AutoInputInt("Object ID", &object_id_, 1, 10, ImGuiInputTextFlags_CharsHexadecimal); + ImGui::SameLine(); + ImGui::TextDisabled("($%03X)", object_id_); + + // Room context + AutoInputInt("Room Context", &room_id_, 1, 10); + ImGui::SameLine(); + ImGui::TextDisabled("(for graphics/palette)"); + + // Position controls + AutoSliderInt("X Position", &object_x_, 0, 63); + AutoSliderInt("Y Position", &object_y_, 0, 63); + + ImGui::Unindent(); + + // Render button - large and prominent + ImGui::Separator(); + if (ImGui::Button("Render Object", ImVec2(-1, 0))) { + TriggerEmulatedRender(); + } + + // Quick test buttons + if (ImGui::BeginPopup("QuickTests")) { + if (ImGui::MenuItem("Floor tile (0x00)")) { object_id_ = 0x00; TriggerEmulatedRender(); } + if (ImGui::MenuItem("Wall N (0x60)")) { object_id_ = 0x60; TriggerEmulatedRender(); } + if (ImGui::MenuItem("Door (0xF0)")) { object_id_ = 0xF0; TriggerEmulatedRender(); } + ImGui::EndPopup(); + } + if (ImGui::Button("Quick Tests...", ImVec2(-1, 0))) { + ImGui::OpenPopup("QuickTests"); + } +} + +void DungeonObjectEmulatorPreview::TriggerEmulatedRender() { + if (!rom_ || !rom_->is_loaded()) { + last_error_ = "ROM not loaded"; + return; + } + + last_error_.clear(); + last_cycle_count_ = 0; + + // 1. Reset and configure the SNES state + snes_instance_->Reset(true); + auto& cpu = snes_instance_->cpu(); + auto& ppu = snes_instance_->ppu(); + auto& memory = snes_instance_->memory(); + + // 2. Load room context (graphics, palettes) + zelda3::Room default_room = zelda3::LoadRoomFromRom(rom_, room_id_); + + // 3. Load palette into CGRAM + auto dungeon_main_pal_group = rom_->palette_group().dungeon_main; + + // Validate and clamp palette ID + int palette_id = default_room.palette; + if (palette_id < 0 || palette_id >= static_cast(dungeon_main_pal_group.size())) { + printf("[EMU] Warning: Room palette %d out of bounds, using palette 0\n", palette_id); + palette_id = 0; + } + + auto palette = dungeon_main_pal_group[palette_id]; + for (size_t i = 0; i < palette.size() && i < 256; ++i) { + ppu.cgram[i] = palette[i].snes(); + } + + // 4. Load graphics into VRAM + default_room.LoadRoomGraphics(default_room.blockset); + default_room.CopyRoomGraphicsToBuffer(); + const auto& gfx_buffer = default_room.get_gfx_buffer(); + for (size_t i = 0; i < gfx_buffer.size() / 2 && i < 0x8000; ++i) { + ppu.vram[i] = gfx_buffer[i * 2] | (gfx_buffer[i * 2 + 1] << 8); + } + + // 5. CRITICAL: Initialize tilemap buffers in WRAM + // Game uses $7E:2000 for BG1 tilemap buffer, $7E:4000 for BG2 + for (uint32_t i = 0; i < 0x2000; i++) { + snes_instance_->Write(0x7E2000 + i, 0x00); // BG1 tilemap buffer + snes_instance_->Write(0x7E4000 + i, 0x00); // BG2 tilemap buffer + } + + // 6. Setup PPU registers for dungeon rendering + snes_instance_->Write(0x002105, 0x09); // BG Mode 1 (4bpp for BG1/2) + snes_instance_->Write(0x002107, 0x40); // BG1 tilemap at VRAM $4000 (32x32) + snes_instance_->Write(0x002108, 0x48); // BG2 tilemap at VRAM $4800 (32x32) + snes_instance_->Write(0x002109, 0x00); // BG1 chr data at VRAM $0000 + snes_instance_->Write(0x00210A, 0x00); // BG2 chr data at VRAM $0000 + snes_instance_->Write(0x00212C, 0x03); // Enable BG1+BG2 on main screen + snes_instance_->Write(0x002100, 0x0F); // Screen display on, full brightness + + // 7. Setup WRAM variables for drawing context + snes_instance_->Write(0x7E00AF, room_id_ & 0xFF); + snes_instance_->Write(0x7E049C, 0x00); + snes_instance_->Write(0x7E049E, 0x00); + + // 8. Create object and encode to bytes + zelda3::RoomObject obj(object_id_, object_x_, object_y_, 0, 0); + auto bytes = obj.EncodeObjectToBytes(); + + const uint32_t object_data_addr = 0x7E1000; + snes_instance_->Write(object_data_addr, bytes.b1); + snes_instance_->Write(object_data_addr + 1, bytes.b2); + snes_instance_->Write(object_data_addr + 2, bytes.b3); + snes_instance_->Write(object_data_addr + 3, 0xFF); // Terminator + snes_instance_->Write(object_data_addr + 4, 0xFF); + + // 9. Setup object pointer in WRAM + snes_instance_->Write(0x7E00B7, object_data_addr & 0xFF); + snes_instance_->Write(0x7E00B8, (object_data_addr >> 8) & 0xFF); + snes_instance_->Write(0x7E00B9, (object_data_addr >> 16) & 0xFF); + + // 10. Setup CPU state + cpu.PB = 0x01; + cpu.DB = 0x7E; + cpu.D = 0x0000; + cpu.SetSP(0x01FF); + cpu.status = 0x30; // 8-bit mode + + // Calculate X register (tilemap position) + cpu.X = (object_y_ * 0x80) + (object_x_ * 2); + cpu.Y = 0; // Object data offset + + // 11. Lookup the object's drawing handler + uint16_t handler_offset = 0; + auto rom_data = rom_->data(); + uint32_t table_addr = 0; + + if (object_id_ < 0x100) { + table_addr = 0x018200 + (object_id_ * 2); + } else if (object_id_ < 0x200) { + table_addr = 0x018470 + ((object_id_ - 0x100) * 2); + } else { + table_addr = 0x0185F0 + ((object_id_ - 0x200) * 2); + } + + if (table_addr < rom_->size() - 1) { + uint8_t lo = rom_data[table_addr]; + uint8_t hi = rom_data[table_addr + 1]; + handler_offset = lo | (hi << 8); + } else { + last_error_ = "Object ID out of bounds for handler lookup"; + return; + } + + if (handler_offset == 0x0000) { + char buf[256]; + snprintf(buf, sizeof(buf), "Object $%04X has no drawing routine", object_id_); + last_error_ = buf; + return; + } + + // 12. Setup return address and jump to handler + const uint16_t return_addr = 0x8000; + snes_instance_->Write(0x018000, 0x6B); // RTL instruction (0x6B not 0x60!) + + // Push return address for RTL (3 bytes: bank, high, low) + uint16_t sp = cpu.SP(); + snes_instance_->Write(0x010000 | sp--, 0x01); // Bank byte + snes_instance_->Write(0x010000 | sp--, (return_addr - 1) >> 8); // High + snes_instance_->Write(0x010000 | sp--, (return_addr - 1) & 0xFF); // Low + cpu.SetSP(sp); + + // Jump to handler (offset is relative to RoomDrawObjectData base) + cpu.PC = handler_offset; + + printf("[EMU] Rendering object $%04X at (%d,%d), handler=$%04X\n", + object_id_, object_x_, object_y_, handler_offset); + + // 13. Run emulator with timeout + int max_cycles = 100000; + int cycles = 0; + while (cycles < max_cycles) { + if (cpu.PB == 0x01 && cpu.PC == return_addr) { + break; // Hit return address + } + snes_instance_->RunCycle(); + cycles++; + } + + last_cycle_count_ = cycles; + + printf("[EMU] Completed after %d cycles, PC=$%02X:%04X\n", + cycles, cpu.PB, cpu.PC); + + if (cycles >= max_cycles) { + last_error_ = "Timeout: exceeded max cycles"; + return; + } + + // 14. Force PPU to render the tilemaps + ppu.HandleFrameStart(); + for (int line = 0; line < 224; line++) { + ppu.RunLine(line); + } + ppu.HandleVblank(); + + // 15. Get the rendered pixels from PPU + void* pixels = nullptr; + int pitch = 0; + if (SDL_LockTexture(object_texture_, nullptr, &pixels, &pitch) == 0) { + snes_instance_->SetPixels(static_cast(pixels)); + SDL_UnlockTexture(object_texture_); + } +} + +} // namespace gui +} // namespace yaze diff --git a/src/app/gui/widgets/dungeon_object_emulator_preview.h b/src/app/gui/widgets/dungeon_object_emulator_preview.h new file mode 100644 index 00000000..78bdaf35 --- /dev/null +++ b/src/app/gui/widgets/dungeon_object_emulator_preview.h @@ -0,0 +1,40 @@ +#ifndef YAZE_APP_GUI_WIDGETS_DUNGEON_OBJECT_EMULATOR_PREVIEW_H_ +#define YAZE_APP_GUI_WIDGETS_DUNGEON_OBJECT_EMULATOR_PREVIEW_H_ + +#include "app/emu/snes.h" +#include "app/rom.h" + +namespace yaze { +namespace gui { + +class DungeonObjectEmulatorPreview { + public: + DungeonObjectEmulatorPreview(); + ~DungeonObjectEmulatorPreview(); + + void Initialize(Rom* rom); + void Render(); + + private: + void RenderControls(); + void TriggerEmulatedRender(); + + Rom* rom_ = nullptr; + std::unique_ptr snes_instance_; + SDL_Texture* object_texture_ = nullptr; + + int object_id_ = 0; + int room_id_ = 0; + int object_x_ = 16; + int object_y_ = 16; + bool show_window_ = true; + + // Debug info + int last_cycle_count_ = 0; + std::string last_error_; +}; + +} // namespace gui +} // namespace yaze + +#endif // YAZE_APP_GUI_WIDGETS_DUNGEON_OBJECT_EMULATOR_PREVIEW_H_ diff --git a/src/app/zelda3/dungeon/object_drawer.cc b/src/app/zelda3/dungeon/object_drawer.cc new file mode 100644 index 00000000..1c104571 --- /dev/null +++ b/src/app/zelda3/dungeon/object_drawer.cc @@ -0,0 +1,220 @@ +#include "object_drawer.h" + +#include + +#include "absl/strings/str_format.h" +#include "app/gfx/snes_tile.h" + +namespace yaze { +namespace zelda3 { + +ObjectDrawer::ObjectDrawer(Rom* rom) : rom_(rom) {} + +absl::Status ObjectDrawer::DrawObject(const RoomObject& object, + gfx::BackgroundBuffer& bg1, + gfx::BackgroundBuffer& bg2) { + if (!rom_ || !rom_->is_loaded()) { + return absl::FailedPreconditionError("ROM not loaded"); + } + + // Ensure object has tiles loaded + auto mutable_obj = const_cast(object); + mutable_obj.set_rom(rom_); + mutable_obj.EnsureTilesLoaded(); + + // Get tiles - silently skip objects that can't load tiles + if (object.tiles().empty()) { + // Many objects may not have tiles loaded yet - this is normal + // Just skip them rather than failing the whole draw operation + return absl::OkStatus(); + } + + const auto& tile = object.tiles()[0]; // Base tile for object + + // Select buffer based on layer + auto& target_bg = (object.layer_ == RoomObject::LayerType::BG2) ? bg2 : bg1; + + // Dispatch to pattern-specific drawing based on object ID + // This is reverse-engineered from the game's drawing routines + + if (object.id_ == 0x34) { + // Object 0x34: 1x1 solid block (simplest) + Draw1x1Solid(object, target_bg, tile); + } + else if (object.id_ >= 0x00 && object.id_ <= 0x08) { + // Objects 0x00-0x08: Rightward 2x2 patterns + DrawRightwards2x2(object, target_bg, tile); + } + else if (object.id_ >= 0x60 && object.id_ <= 0x68) { + // Objects 0x60-0x68: Downward 2x2 patterns + DrawDownwards2x2(object, target_bg, tile); + } + else if (object.id_ >= 0x09 && object.id_ <= 0x14) { + // Objects 0x09-0x14: Diagonal acute patterns + DrawDiagonalAcute(object, target_bg, tile); + } + else if (object.id_ >= 0x15 && object.id_ <= 0x20) { + // Objects 0x15-0x20: Diagonal grave patterns + DrawDiagonalGrave(object, target_bg, tile); + } + else if (object.id_ == 0x33 || (object.id_ >= 0x70 && object.id_ <= 0x71)) { + // 4x4 block objects + Draw4x4Block(object, target_bg, tile); + } + else { + // Default: Draw as simple 1x1 at position + Draw1x1Solid(object, target_bg, tile); + } + + return absl::OkStatus(); +} + +absl::Status ObjectDrawer::DrawObjectList( + const std::vector& objects, + gfx::BackgroundBuffer& bg1, + gfx::BackgroundBuffer& bg2) { + + int drawn_count = 0; + int skipped_count = 0; + + for (const auto& object : objects) { + auto status = DrawObject(object, bg1, bg2); + if (status.ok()) { + drawn_count++; + } else { + skipped_count++; + // Only print errors that aren't "no tiles" (which is common and expected) + if (status.code() != absl::StatusCode::kOk) { + // Skip silently - many objects don't have tiles loaded yet + } + } + } + + if (drawn_count > 0 || skipped_count > 0) { + printf("[ObjectDrawer] Drew %d objects, skipped %d\n", drawn_count, skipped_count); + } + + return absl::OkStatus(); +} + +// ============================================================================ +// Pattern Drawing Implementations +// ============================================================================ + +void ObjectDrawer::Draw1x1Solid(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Simple 1x1 tile placement + WriteTile16(bg, obj.x_, obj.y_, tile); +} + +void ObjectDrawer::DrawRightwards2x2(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Pattern: Draws 2x2 tiles rightward + // Size byte determines how many times to repeat + int repeat_count = (obj.size_ & 0x0F) + 1; // Low nibble = width + + for (int i = 0; i < repeat_count; i++) { + // Each iteration draws a 2x2 tile16 + int tile_x = obj.x_ + (i * 2); // Each tile16 is 2x2 8x8 tiles + int tile_y = obj.y_; + + WriteTile16(bg, tile_x, tile_y, tile); + } +} + +void ObjectDrawer::DrawDownwards2x2(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Pattern: Draws 2x2 tiles downward + // Size byte determines how many times to repeat + int repeat_count = ((obj.size_ >> 4) & 0x0F) + 1; // High nibble = height + + for (int i = 0; i < repeat_count; i++) { + int tile_x = obj.x_; + int tile_y = obj.y_ + (i * 2); + + WriteTile16(bg, tile_x, tile_y, tile); + } +} + +void ObjectDrawer::DrawDiagonalAcute(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Pattern: Diagonal line going down-right (/) + int length = (obj.size_ & 0x0F) + 1; + + for (int i = 0; i < length; i++) { + int tile_x = obj.x_ + i; + int tile_y = obj.y_ + i; + + WriteTile16(bg, tile_x, tile_y, tile); + } +} + +void ObjectDrawer::DrawDiagonalGrave(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Pattern: Diagonal line going down-left (\) + int length = (obj.size_ & 0x0F) + 1; + + for (int i = 0; i < length; i++) { + int tile_x = obj.x_ - i; + int tile_y = obj.y_ + i; + + WriteTile16(bg, tile_x, tile_y, tile); + } +} + +void ObjectDrawer::Draw4x4Block(const RoomObject& obj, + gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile) { + // Pattern: 4x4 tile16 block (8x8 8x8 tiles total) + for (int yy = 0; yy < 4; yy++) { + for (int xx = 0; xx < 4; xx++) { + int tile_x = obj.x_ + (xx * 2); + int tile_y = obj.y_ + (yy * 2); + + WriteTile16(bg, tile_x, tile_y, tile); + } + } +} + +// ============================================================================ +// Utility Methods +// ============================================================================ + +void ObjectDrawer::WriteTile16(gfx::BackgroundBuffer& bg, int tile_x, int tile_y, + const gfx::Tile16& tile) { + // A Tile16 is 2x2 8x8 tiles, so we write 4 tile entries + + // Top-left (tile0) + if (IsValidTilePosition(tile_x, tile_y)) { + bg.SetTileAt(tile_x, tile_y, gfx::TileInfoToWord(tile.tile0_)); + } + + // Top-right (tile1) + if (IsValidTilePosition(tile_x + 1, tile_y)) { + bg.SetTileAt(tile_x + 1, tile_y, gfx::TileInfoToWord(tile.tile1_)); + } + + // Bottom-left (tile2) + if (IsValidTilePosition(tile_x, tile_y + 1)) { + bg.SetTileAt(tile_x, tile_y + 1, gfx::TileInfoToWord(tile.tile2_)); + } + + // Bottom-right (tile3) + if (IsValidTilePosition(tile_x + 1, tile_y + 1)) { + bg.SetTileAt(tile_x + 1, tile_y + 1, gfx::TileInfoToWord(tile.tile3_)); + } +} + +bool ObjectDrawer::IsValidTilePosition(int tile_x, int tile_y) const { + return tile_x >= 0 && tile_x < kMaxTilesX && + tile_y >= 0 && tile_y < kMaxTilesY; +} + +} // namespace zelda3 +} // namespace yaze + diff --git a/src/app/zelda3/dungeon/object_drawer.h b/src/app/zelda3/dungeon/object_drawer.h new file mode 100644 index 00000000..76d13933 --- /dev/null +++ b/src/app/zelda3/dungeon/object_drawer.h @@ -0,0 +1,84 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H +#define YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H + +#include + +#include "absl/status/status.h" +#include "app/gfx/background_buffer.h" +#include "app/gfx/snes_tile.h" +#include "app/rom.h" +#include "app/zelda3/dungeon/room_object.h" + +namespace yaze { +namespace zelda3 { + +/** + * @brief Draws dungeon objects to background buffers using game patterns + * + * This class interprets object IDs and draws them to BG1/BG2 buffers + * using the patterns extracted from the game's drawing routines. + * + * Architecture: + * 1. Load tile data from ROM for the object + * 2. Determine drawing pattern (rightward, downward, diagonal, special) + * 3. Write tiles to BackgroundBuffer according to pattern + * 4. Handle size bytes for repeating patterns + */ +class ObjectDrawer { + public: + explicit ObjectDrawer(Rom* rom); + + /** + * @brief Draw a room object to background buffers + * @param object The object to draw + * @param bg1 Background layer 1 buffer + * @param bg2 Background layer 2 buffer + * @return Status of the drawing operation + */ + absl::Status DrawObject(const RoomObject& object, + gfx::BackgroundBuffer& bg1, + gfx::BackgroundBuffer& bg2); + + /** + * @brief Draw all objects in a room + * @param objects Vector of room objects + * @param bg1 Background layer 1 buffer + * @param bg2 Background layer 2 buffer + * @return Status of the drawing operation + */ + absl::Status DrawObjectList(const std::vector& objects, + gfx::BackgroundBuffer& bg1, + gfx::BackgroundBuffer& bg2); + + private: + // Pattern-specific drawing methods + void DrawRightwards2x2(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + void DrawDownwards2x2(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + void DrawDiagonalAcute(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + void DrawDiagonalGrave(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + void Draw1x1Solid(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + void Draw4x4Block(const RoomObject& obj, gfx::BackgroundBuffer& bg, + const gfx::Tile16& tile); + + // Utility methods + void WriteTile16(gfx::BackgroundBuffer& bg, int tile_x, int tile_y, + const gfx::Tile16& tile); + bool IsValidTilePosition(int tile_x, int tile_y) const; + + Rom* rom_; + + // Canvas dimensions in tiles (64x64 = 512x512 pixels) + static constexpr int kMaxTilesX = 64; + static constexpr int kMaxTilesY = 64; +}; + +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_DUNGEON_OBJECT_DRAWER_H + diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index defe7d21..dc8b790c 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -310,30 +310,38 @@ void Room::RenderRoomGraphics() { int num_palettes = dungeon_pal_group.size(); int palette_id = palette; + std::printf("5a. Dungeon palette group has %d palettes total\n", num_palettes); + // Validate palette ID and fall back to palette 0 if invalid if (palette_id < 0 || palette_id >= num_palettes) { - //palette_id = 0; + std::printf("5a. WARNING: palette_id %d is out of bounds [0, %d), using palette 0\n", + palette_id, num_palettes); + palette_id = 0; } // Load the 90-color dungeon palette directly // The palette contains colors for BG layers - sprite colors are handled separately - auto bg1_palette = dungeon_pal_group.palette(palette_id); + auto bg1_palette = dungeon_pal_group[palette_id]; // Use operator[] to get a proper reference std::printf("5a. Palette loaded: room palette_id=%d (requested=%d), size=%zu colors\n", palette_id, palette, bg1_palette.size()); - // CRITICAL: Apply palette to bitmaps BEFORE creating/updating textures - bg1_bmp.SetPaletteWithTransparent(bg1_palette, 0); - bg2_bmp.SetPaletteWithTransparent(bg1_palette, 0); - std::printf("5b. Palette applied to bitmaps\n"); + // CRITICAL: Only apply palette if it's valid + if (bg1_palette.size() > 0) { + bg1_bmp.SetPaletteWithTransparent(bg1_palette, 0); + bg2_bmp.SetPaletteWithTransparent(bg1_palette, 0); + std::printf("5b. Palette applied to bitmaps\n"); + } else { + std::printf("5b. WARNING: Palette is empty, skipping SetPalette\n"); + } // ALWAYS recreate textures when palette changes (UpdateBitmap doesn't update palette!) std::printf("6. Recreating bitmap textures with new palette\n"); core::Renderer::Get().CreateAndRenderBitmap( - 0x200, 0x200, 0x200, gfx::Arena::Get().bg1().bitmap().vector(), + 0x200, 0x200, 8, gfx::Arena::Get().bg1().bitmap().vector(), gfx::Arena::Get().bg1().bitmap(), bg1_palette); core::Renderer::Get().CreateAndRenderBitmap( - 0x200, 0x200, 0x200, gfx::Arena::Get().bg2().bitmap().vector(), + 0x200, 0x200, 8, gfx::Arena::Get().bg2().bitmap().vector(), gfx::Arena::Get().bg2().bitmap(), bg1_palette); std::printf("7. BG1 has texture: %d\n", bg1_bmp.texture() != nullptr); diff --git a/src/app/zelda3/dungeon/room.h b/src/app/zelda3/dungeon/room.h index b5d1ae67..6dc9cb15 100644 --- a/src/app/zelda3/dungeon/room.h +++ b/src/app/zelda3/dungeon/room.h @@ -347,6 +347,7 @@ class Room { auto& mutable_blocks() { return blocks_; } auto rom() { return rom_; } auto mutable_rom() { return rom_; } + const std::array& get_gfx_buffer() const { return current_gfx16_; } private: Rom* rom_; diff --git a/src/app/zelda3/zelda3_library.cmake b/src/app/zelda3/zelda3_library.cmake index 6789e51d..559e76b7 100644 --- a/src/app/zelda3/zelda3_library.cmake +++ b/src/app/zelda3/zelda3_library.cmake @@ -14,6 +14,7 @@ set( app/zelda3/dungeon/room_object.cc app/zelda3/dungeon/object_parser.cc app/zelda3/dungeon/object_renderer.cc + app/zelda3/dungeon/object_drawer.cc app/zelda3/dungeon/room_layout.cc app/zelda3/dungeon/room_diagnostic.cc app/zelda3/dungeon/room_visual_diagnostic.cc