diff --git a/src/app/editor/dungeon/dungeon_editor.cc b/src/app/editor/dungeon/dungeon_editor.cc index 9c8829fd..a91b01ac 100644 --- a/src/app/editor/dungeon/dungeon_editor.cc +++ b/src/app/editor/dungeon/dungeon_editor.cc @@ -13,6 +13,7 @@ #include "app/zelda3/dungeon/dungeon_editor_system.h" #include "app/zelda3/dungeon/dungeon_object_editor.h" #include "app/zelda3/dungeon/room.h" +#include "app/zelda3/dungeon/room_visual_diagnostic.h" #include "imgui/imgui.h" namespace yaze::editor { @@ -283,6 +284,36 @@ void DungeonEditor::DrawCanvasAndPropertiesPanel() { ImGui::EndTabItem(); } + // Visual Diagnostic tab - for debugging rendering + if (ImGui::BeginTabItem("Visual Diagnostic")) { + if (!active_rooms_.empty()) { + int room_id = active_rooms_[current_active_room_tab_]; + auto& room = rooms_[room_id]; + + // Show button to toggle diagnostic window + if (ImGui::Button("Open Diagnostic Window")) { + show_visual_diagnostic_ = true; + } + + // Render visual diagnostic + if (show_visual_diagnostic_) { + // Get the global graphics buffer for tile decoding + static std::vector empty_gfx; + const auto& gfx_buffer = rom()->graphics_buffer(); + + zelda3::dungeon::RoomVisualDiagnostic::DrawDiagnosticWindow( + &show_visual_diagnostic_, + gfx::Arena::Get().bg1(), + gfx::Arena::Get().bg2(), + current_palette_, + gfx_buffer.empty() ? empty_gfx : gfx_buffer); + } + } else { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "No room selected. Open a room to see diagnostics."); + } + ImGui::EndTabItem(); + } + // Room Properties tab - debug and editing controls if (ImGui::BeginTabItem("Room Properties")) { if (ImGui::Button("Room Debug Info")) { diff --git a/src/app/editor/dungeon/dungeon_editor.h b/src/app/editor/dungeon/dungeon_editor.h index c77b5ecc..482e9c80 100644 --- a/src/app/editor/dungeon/dungeon_editor.h +++ b/src/app/editor/dungeon/dungeon_editor.h @@ -146,6 +146,7 @@ class DungeonEditor : public Editor { bool show_door_editor_ = false; bool show_chest_editor_ = false; bool show_properties_editor_ = false; + bool show_visual_diagnostic_ = false; uint16_t current_entrance_id_ = 0; uint16_t current_room_id_ = 0; diff --git a/src/app/gfx/background_buffer.cc b/src/app/gfx/background_buffer.cc index 6579b1fb..c7ae471d 100644 --- a/src/app/gfx/background_buffer.cc +++ b/src/app/gfx/background_buffer.cc @@ -36,7 +36,15 @@ void BackgroundBuffer::ClearBuffer() { std::ranges::fill(buffer_, 0); } void BackgroundBuffer::DrawTile(const TileInfo& tile, uint8_t* canvas, const uint8_t* tiledata, int indexoffset) { int tx = (tile.id_ / 16 * 512) + ((tile.id_ & 0xF) * 4); - uint8_t palnibble = (uint8_t)(tile.palette_ << 4); + + // Clamp palette to 0-5 (90 colors / 16 = 5.625, so max palette is 5) + // Palettes 6-7 would require colors 96-127, which don't exist in dungeon palettes + uint8_t clamped_palette = tile.palette_ & 0x07; // Get palette 0-7 + if (clamped_palette > 5) { + clamped_palette = clamped_palette % 6; // Wrap palette 6->0, 7->1 + } + + uint8_t palnibble = (uint8_t)(clamped_palette << 4); uint8_t r = tile.horizontal_mirror_ ? 1 : 0; for (int yl = 0; yl < 512; yl += 64) { diff --git a/src/app/zelda3/dungeon/room.cc b/src/app/zelda3/dungeon/room.cc index c51c8463..efb3897f 100644 --- a/src/app/zelda3/dungeon/room.cc +++ b/src/app/zelda3/dungeon/room.cc @@ -310,11 +310,25 @@ void Room::RenderRoomGraphics() { std::printf("5. BG1 bitmap: active=%d, size=%dx%d, data_size=%zu\n", bg1_bmp.is_active(), bg1_bmp.width(), bg1_bmp.height(), bg1_bmp.vector().size()); - // Get the palette for this room - auto bg1_palette = - rom()->mutable_palette_group()->get_group("dungeon_main")[0].palette(0); + // Get the palette for this room - just use the 90-color palette as-is + // The SNES will index into this palette correctly without needing expansion + auto& dungeon_pal_group = rom()->mutable_palette_group()->get_group("dungeon_main")[0]; + int num_palettes = dungeon_pal_group.size(); + int palette_id = palette; - std::printf("5a. Palette loaded: size=%zu colors\n", bg1_palette.size()); + // Validate palette ID and fall back to palette 0 if invalid + if (palette_id < 0 || palette_id >= num_palettes) { + std::printf("WARNING: Room %d has invalid palette_id=%d (max=%d), falling back to palette 0\n", + room_id_, palette_id, num_palettes - 1); + 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); + + 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); diff --git a/src/app/zelda3/dungeon/room_visual_diagnostic.cc b/src/app/zelda3/dungeon/room_visual_diagnostic.cc new file mode 100644 index 00000000..e823b4f4 --- /dev/null +++ b/src/app/zelda3/dungeon/room_visual_diagnostic.cc @@ -0,0 +1,330 @@ +#include "app/zelda3/dungeon/room_visual_diagnostic.h" + +#include +#include +#include +#include +#include + +#include "absl/strings/str_format.h" +#include "app/gfx/snes_tile.h" +#include "imgui/imgui.h" + +namespace yaze { +namespace zelda3 { +namespace dungeon { + +void RoomVisualDiagnostic::DrawDiagnosticWindow( + bool* p_open, + gfx::BackgroundBuffer& bg1_buffer, + gfx::BackgroundBuffer& bg2_buffer, + const gfx::SnesPalette& palette, + const std::vector& gfx16_data) { + + if (!ImGui::Begin("Room Rendering Diagnostic", p_open, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::End(); + return; + } + + if (ImGui::CollapsingHeader("Texture Previews", ImGuiTreeNodeFlags_DefaultOpen)) { + DrawTexturePreview(bg1_buffer.bitmap(), "BG1 Texture"); + ImGui::Separator(); + DrawTexturePreview(bg2_buffer.bitmap(), "BG2 Texture"); + } + + if (ImGui::CollapsingHeader("Palette Inspector")) { + DrawPaletteInspector(palette); + } + + if (ImGui::CollapsingHeader("Tile Buffer Inspector")) { + if (ImGui::BeginTabBar("BufferTabs")) { + if (ImGui::BeginTabItem("BG1 Buffer")) { + DrawTileBufferInspector(bg1_buffer, palette); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("BG2 Buffer")) { + DrawTileBufferInspector(bg2_buffer, palette); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + + if (ImGui::CollapsingHeader("Pixel Inspector")) { + if (ImGui::BeginTabBar("PixelTabs")) { + if (ImGui::BeginTabItem("BG1 Pixels")) { + DrawPixelInspector(bg1_buffer.bitmap(), palette); + ImGui::EndTabItem(); + } + if (ImGui::BeginTabItem("BG2 Pixels")) { + DrawPixelInspector(bg2_buffer.bitmap(), palette); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + + if (ImGui::CollapsingHeader("Tile Decoder")) { + static int test_tile_id = 0xEE; + ImGui::InputInt("Tile ID (hex)", &test_tile_id, 1, 16, ImGuiInputTextFlags_CharsHexadecimal); + test_tile_id = std::clamp(test_tile_id, 0, 0x1FF); + DrawTileDecoder(gfx16_data, test_tile_id); + } + + ImGui::End(); +} + +void RoomVisualDiagnostic::DrawTexturePreview(const gfx::Bitmap& bitmap, const char* label) { + ImGui::Text("%s", label); + + if (!bitmap.is_active()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Bitmap not active"); + return; + } + + ImGui::Text("Size: %dx%d, Data: %zu bytes", + bitmap.width(), bitmap.height(), bitmap.vector().size()); + + // Show texture if available + if (bitmap.texture()) { + ImVec2 preview_size(256, 256); // Quarter size preview + ImGui::Image(bitmap.texture(), preview_size); + + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + // Zoomed preview on hover + ImVec2 zoom_size(512, 512); + ImGui::Image(bitmap.texture(), zoom_size); + ImGui::EndTooltip(); + } + } else { + ImGui::TextColored(ImVec4(1, 1, 0, 1), "No texture available"); + } +} + +void RoomVisualDiagnostic::DrawPaletteInspector(const gfx::SnesPalette& palette) { + ImGui::Text("Palette size: %zu colors", palette.size()); + + int cols = 16; + for (size_t i = 0; i < palette.size(); i++) { + if (i % cols != 0) ImGui::SameLine(); + + auto color = palette[i]; + auto rgb = color.rgb(); + ImVec4 imcolor(rgb.x, rgb.y, rgb.z, 1.0f); + + ImGui::PushID(static_cast(i)); + if (ImGui::ColorButton("##color", imcolor, + ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker, + ImVec2(20, 20))) { + // Clicked - could add inspection + } + ImGui::PopID(); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("[%zu] SNES:0x%04X RGB:(%.0f,%.0f,%.0f)", + i, color.snes(), + rgb.x * 255, rgb.y * 255, rgb.z * 255); + } + } +} + +void RoomVisualDiagnostic::DrawTileBufferInspector(gfx::BackgroundBuffer& buffer, + const gfx::SnesPalette& palette) { + const auto& tile_buffer = buffer.buffer(); + const auto& bitmap = buffer.bitmap(); + int tiles_w = bitmap.width() / 8; + int tiles_h = bitmap.height() / 8; + + ImGui::Text("Buffer size: %zu tiles (%d x %d)", tile_buffer.size(), tiles_w, tiles_h); + + // Count non-empty tiles + int non_empty = 0; + std::set unique_tiles; + for (auto word : tile_buffer) { + if (word != 0xFFFF && word != 0) { + non_empty++; + unique_tiles.insert(word); + } + } + + ImGui::Text("Non-empty tiles: %d / %zu", non_empty, tile_buffer.size()); + ImGui::Text("Unique tile words: %zu", unique_tiles.size()); + + // Sample tiles + ImGui::Separator(); + ImGui::Text("First 20 tiles:"); + + static int selected_tile = -1; + for (int i = 0; i < std::min(20, tile_buffer.size()); i++) { + uint16_t word = tile_buffer[i]; + auto tile = gfx::WordToTileInfo(word); + + bool is_selected = (selected_tile == i); + if (ImGui::Selectable(absl::StrFormat("[%d] Word:0x%04X ID:%d Pal:%d H:%d V:%d P:%d", + i, word, tile.id_, tile.palette_, + tile.horizontal_mirror_, tile.vertical_mirror_, + tile.over_).c_str(), + is_selected)) { + selected_tile = i; + } + } + + if (selected_tile >= 0 && selected_tile < tile_buffer.size()) { + ImGui::Separator(); + ImGui::Text("Selected Tile %d:", selected_tile); + uint16_t word = tile_buffer[selected_tile]; + auto tile = gfx::WordToTileInfo(word); + + ImGui::BulletText("Word: 0x%04X", word); + ImGui::BulletText("Tile ID: 0x%03X (%d)", tile.id_, tile.id_); + ImGui::BulletText("Palette: %d", tile.palette_); + ImGui::BulletText("H-Mirror: %d, V-Mirror: %d", + tile.horizontal_mirror_, tile.vertical_mirror_); + ImGui::BulletText("Priority: %d", tile.over_); + + // Calculate palette color range for this tile + int pal_start = tile.palette_ * 16; + int pal_end = pal_start + 15; + ImGui::BulletText("Palette range: colors %d-%d", pal_start, pal_end); + + if (pal_end >= palette.size()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), + "WARNING: Palette range exceeds palette size (%zu)!", + palette.size()); + } + } +} + +void RoomVisualDiagnostic::DrawPixelInspector(const gfx::Bitmap& bitmap, + const gfx::SnesPalette& palette) { + if (!bitmap.is_active() || bitmap.vector().empty()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Bitmap not active or empty"); + return; + } + + const auto& pixels = bitmap.vector(); + + // Analyze pixels + std::map color_histogram; + for (size_t i = 0; i < std::min(1000, pixels.size()); i++) { + color_histogram[pixels[i]]++; + } + + ImGui::Text("Pixel analysis (first 1000 pixels):"); + ImGui::Text("Unique colors: %zu", color_histogram.size()); + + ImGui::Separator(); + ImGui::Text("Color distribution:"); + + // Show top 10 most common colors + std::vector> sorted_colors(color_histogram.begin(), color_histogram.end()); + std::sort(sorted_colors.begin(), sorted_colors.end(), + [](const auto& a, const auto& b) { return a.second > b.second; }); + + for (size_t i = 0; i < std::min(10, sorted_colors.size()); i++) { + uint8_t idx = sorted_colors[i].first; + int count = sorted_colors[i].second; + + ImGui::Text("[%3d] Count: %4d (%.1f%%)", idx, count, (count * 100.0f) / 1000.0f); + + if (idx < palette.size()) { + ImGui::SameLine(); + auto color = palette[idx]; + auto rgb = color.rgb(); + ImVec4 imcolor(rgb.x, rgb.y, rgb.z, 1.0f); + ImGui::ColorButton("##pal", imcolor, ImGuiColorEditFlags_NoAlpha, ImVec2(16, 16)); + + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("SNES:0x%04X RGB:(%.0f,%.0f,%.0f)", + color.snes(), rgb.x * 255, rgb.y * 255, rgb.z * 255); + } + } else { + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1, 0, 0, 1), "OUT OF RANGE!"); + } + } + + // Interactive pixel inspector + ImGui::Separator(); + static int inspect_x = 0; + static int inspect_y = 0; + + ImGui::Text("Pixel Inspector:"); + ImGui::SliderInt("X", &inspect_x, 0, bitmap.width() - 1); + ImGui::SliderInt("Y", &inspect_y, 0, bitmap.height() - 1); + + int pixel_idx = inspect_y * bitmap.width() + inspect_x; + if (pixel_idx >= 0 && pixel_idx < pixels.size()) { + uint8_t pal_idx = pixels[pixel_idx]; + ImGui::Text("Pixel (%d, %d) = Palette Index %d", inspect_x, inspect_y, pal_idx); + + if (pal_idx < palette.size()) { + auto color = palette[pal_idx]; + auto rgb = color.rgb(); + ImVec4 imcolor(rgb.x, rgb.y, rgb.z, 1.0f); + ImGui::ColorButton("##pixel_color", imcolor, ImGuiColorEditFlags_NoAlpha, ImVec2(40, 40)); + ImGui::SameLine(); + ImGui::Text("SNES:0x%04X RGB:(%.0f,%.0f,%.0f)", + color.snes(), rgb.x * 255, rgb.y * 255, rgb.z * 255); + } else { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Palette index %d out of range (max: %zu)!", + pal_idx, palette.size() - 1); + } + } +} + +void RoomVisualDiagnostic::DrawTileDecoder(const std::vector& gfx16_data, int tile_id) { + if (gfx16_data.empty()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "No graphics data"); + return; + } + + // Calculate tile offset in graphics data + int tx = (tile_id / 16 * 512) + ((tile_id & 0xF) * 4); + + ImGui::Text("Decoding tile 0x%03X", tile_id); + ImGui::Text("Offset in gfx16_data: 0x%X (%d)", tx, tx); + + if (tx < 0 || tx + 32 > gfx16_data.size()) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Tile offset out of range!"); + return; + } + + // Show raw tile data + ImGui::Text("Raw 4bpp data (32 bytes):"); + for (int i = 0; i < 32; i += 8) { + ImGui::Text("%02X %02X %02X %02X %02X %02X %02X %02X", + gfx16_data[tx + i], gfx16_data[tx + i + 1], + gfx16_data[tx + i + 2], gfx16_data[tx + i + 3], + gfx16_data[tx + i + 4], gfx16_data[tx + i + 5], + gfx16_data[tx + i + 6], gfx16_data[tx + i + 7]); + } + + // Decode and visualize 8x8 tile + ImGui::Separator(); + ImGui::Text("Decoded 8x8 pixel values (palette 0):"); + + // Decode the tile manually to show what pixels are produced + for (int y = 0; y < 8; y++) { + std::string line; + for (int x = 0; x < 8; x++) { + int yl = (y / 8) * 64 + (y % 8) * 8; + int xl = x / 2; + + if (tx + yl + xl < gfx16_data.size()) { + uint8_t pixel_byte = gfx16_data[tx + yl + xl]; + uint8_t pixel_val = (x % 2 == 0) ? (pixel_byte >> 4) : (pixel_byte & 0x0F); + line += absl::StrFormat("%X", pixel_val); + } else { + line += "?"; + } + } + ImGui::Text("%s", line.c_str()); + } +} + +} // namespace dungeon +} // namespace zelda3 +} // namespace yaze + diff --git a/src/app/zelda3/dungeon/room_visual_diagnostic.h b/src/app/zelda3/dungeon/room_visual_diagnostic.h new file mode 100644 index 00000000..114f4bac --- /dev/null +++ b/src/app/zelda3/dungeon/room_visual_diagnostic.h @@ -0,0 +1,52 @@ +#ifndef YAZE_APP_ZELDA3_DUNGEON_ROOM_VISUAL_DIAGNOSTIC_H +#define YAZE_APP_ZELDA3_DUNGEON_ROOM_VISUAL_DIAGNOSTIC_H + +#include "imgui/imgui.h" +#include "app/gfx/bitmap.h" +#include "app/gfx/snes_palette.h" +#include "app/gfx/background_buffer.h" + +namespace yaze { +namespace zelda3 { +namespace dungeon { + +/** + * @brief Visual diagnostic tool for dungeon rendering using ImGui + * + * Provides interactive visualization of: + * - Texture previews (BG1, BG2) + * - Bitmap data with zoom + * - Tile buffer contents + * - Palette colors + * - Pixel value inspection + */ +class RoomVisualDiagnostic { + public: + /** + * @brief Draw the diagnostic window + * @param bg1_buffer Background 1 buffer reference + * @param bg2_buffer Background 2 buffer reference + * @param palette Current palette being used + * @param gfx16_data Graphics data buffer + */ + static void DrawDiagnosticWindow( + bool* p_open, + gfx::BackgroundBuffer& bg1_buffer, + gfx::BackgroundBuffer& bg2_buffer, + const gfx::SnesPalette& palette, + const std::vector& gfx16_data); + + private: + static void DrawTexturePreview(const gfx::Bitmap& bitmap, const char* label); + static void DrawPaletteInspector(const gfx::SnesPalette& palette); + static void DrawTileBufferInspector(gfx::BackgroundBuffer& buffer, const gfx::SnesPalette& palette); + static void DrawPixelInspector(const gfx::Bitmap& bitmap, const gfx::SnesPalette& palette); + static void DrawTileDecoder(const std::vector& gfx16_data, int tile_id); +}; + +} // namespace dungeon +} // namespace zelda3 +} // namespace yaze + +#endif // YAZE_APP_ZELDA3_DUNGEON_ROOM_VISUAL_DIAGNOSTIC_H + diff --git a/src/app/zelda3/zelda3.cmake b/src/app/zelda3/zelda3.cmake index 4d07bb91..a0581d8d 100644 --- a/src/app/zelda3/zelda3.cmake +++ b/src/app/zelda3/zelda3.cmake @@ -16,6 +16,7 @@ set( app/zelda3/dungeon/object_renderer.cc app/zelda3/dungeon/room_layout.cc app/zelda3/dungeon/room_diagnostic.cc + app/zelda3/dungeon/room_visual_diagnostic.cc app/zelda3/dungeon/dungeon_editor_system.cc app/zelda3/dungeon/dungeon_object_editor.cc ) \ No newline at end of file