# Gemini Task: Fix Dungeon Object Rendering ## Build Instructions ```bash # Configure and build (use dedicated build_gemini directory) ./scripts/gemini_build.sh # Or manually: cmake --preset mac-gemini cmake --build build_gemini --target yaze -j8 # Run the app to test ./build_gemini/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon # Run all stable tests (GTest executable) ./build_gemini/Debug/yaze_test_stable # Run specific test suites with gtest_filter ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Room*" ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Dungeon*" ./build_gemini/Debug/yaze_test_stable --gtest_filter="*ObjectDrawer*" # List available tests ./build_gemini/Debug/yaze_test_stable --gtest_list_tests ``` --- ## Executive Summary **Root Cause**: The dungeon rendering system has TWO bugs: 1. **Missing 3BPP→4BPP conversion**: ROM data is copied raw without format conversion 2. **Wrong palette offset multiplier**: Uses `* 16` (4BPP) but should use `* 8` (3BPP) **The Correct Fix**: Either: - **Option A**: Convert 3BPP to 4BPP during buffer copy, then `* 16` is correct - **Option B**: Keep raw 3BPP data, change multiplier back to `* 8` ZScream uses Option A (full 4BPP conversion). This document provides the exact algorithm. --- ## Critical Bug Analysis ### Bug #1: Palette Offset Calculation (object_drawer.cc:911) **Current Code (WRONG for 3BPP data):** ```cpp uint8_t palette_offset = (tile_info.palette_ & 0x07) * 16; ``` **What ZScream Does (Reference Implementation):** ```csharp // ZScreamDungeon/GraphicsManager.cs lines 1043-1044 gfx16Pointer[index + r ^ 1] = (byte)((pixel & 0x0F) + (tile.palette * 16)); gfx16Pointer[index + r] = (byte)(((pixel >> 4) & 0x0F) + (tile.palette * 16)); ``` **Key Insight**: ZScream uses `* 16` because it CONVERTS the data to 4BPP first. Without that conversion, yaze should use `* 8`. ### Bug #2: Missing BPP Conversion (room.cc:228-295) **Current Code (Copies raw 3BPP data):** ```cpp void Room::CopyRoomGraphicsToBuffer() { auto gfx_buffer_data = rom()->mutable_graphics_buffer(); int sheet_pos = 0; for (int i = 0; i < 16; i++) { int block_offset = blocks_[i] * kGfxBufferRoomOffset; // 2048 bytes/block while (data < kGfxBufferRoomOffset) { current_gfx16_[data + sheet_pos] = (*gfx_buffer_data)[data + block_offset]; data++; } sheet_pos += kGfxBufferRoomOffset; } } ``` **Problem**: This copies raw bytes without any BPP format conversion! --- ## ZScream Reference Implementation ### Buffer Sizes (GraphicsManager.cs:20-95) ```csharp // Graphics buffer: 32KB (128×512 pixels / 2 nibbles per byte) currentgfx16Ptr = Marshal.AllocHGlobal((128 * 512) / 2) // 32,768 bytes // Room backgrounds: 256KB each (512×512 pixels @ 8BPP) roomBg1Ptr = Marshal.AllocHGlobal(512 * 512) // 262,144 bytes roomBg2Ptr = Marshal.AllocHGlobal(512 * 512) // 262,144 bytes ``` ### Sheet Classification (Constants.cs:20-21) ```csharp Uncompressed3BPPSize = 0x0600 // 1536 bytes per 3BPP sheet (24 bytes/tile × 64 tiles) UncompressedSheetSize = 0x0800 // 2048 bytes per 2BPP sheet // 3BPP sheets: 0-112, 115-126, 127-217 (dungeon/overworld graphics) // 2BPP sheets: 113-114, 218-222 (fonts, UI elements) ``` ### 3BPP to 4BPP Conversion Algorithm (GraphicsManager.cs:379-400) **This is the exact algorithm yaze needs to implement:** ```csharp // For each 3BPP sheet: for (int j = 0; j < 4; j++) { // 4 rows of tiles for (int i = 0; i < 16; i++) { // 16 tiles per row for (int y = 0; y < 8; y++) { // 8 pixel rows per tile // Read 3 bitplanes from ROM (SNES planar format) byte lineBits0 = data[(y * 2) + (i * 24) + (j * 384) + sheetPosition]; byte lineBits1 = data[(y * 2) + (i * 24) + (j * 384) + 1 + sheetPosition]; byte lineBits2 = data[(y) + (i * 24) + (j * 384) + 16 + sheetPosition]; // For each pair of pixels (4 nibbles = 4 pixels, but processed as 2 pairs) for (int x = 0; x < 4; x++) { byte pixdata = 0; byte pixdata2 = 0; // Extract pixel 1 color (bits from all 3 planes) if ((lineBits0 & mask[x * 2]) == mask[x * 2]) pixdata += 1; if ((lineBits1 & mask[x * 2]) == mask[x * 2]) pixdata += 2; if ((lineBits2 & mask[x * 2]) == mask[x * 2]) pixdata += 4; // Extract pixel 2 color if ((lineBits0 & mask[x * 2 + 1]) == mask[x * 2 + 1]) pixdata2 += 1; if ((lineBits1 & mask[x * 2 + 1]) == mask[x * 2 + 1]) pixdata2 += 2; if ((lineBits2 & mask[x * 2 + 1]) == mask[x * 2 + 1]) pixdata2 += 4; // Pack into 4BPP format (2 pixels per byte, 4 bits each) int destIndex = (y * 64) + x + (i * 4) + (j * 512) + (s * 2048); newData[destIndex] = (byte)((pixdata << 4) | pixdata2); } } } sheetPosition += 0x0600; // Advance by 1536 bytes per 3BPP sheet } // Bit extraction mask byte[] mask = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; ``` ### Tile Drawing to Buffer (GraphicsManager.cs:140-164) ```csharp public static void DrawTileToBuffer(Tile tile, byte* canvas, byte* tiledata) { // Calculate tile position in graphics buffer int tx = (tile.ID / 16 * 512) + ((tile.ID & 0xF) * 4); byte palnibble = (byte)(tile.Palette << 4); // Palette offset (0, 16, 32, ...) byte r = tile.HFlipByte; for (int yl = 0; yl < 512; yl += 64) { // Each line is 64 bytes apart int my = (tile.VFlip ? 448 - yl : yl); for (int xl = 0; xl < 4; xl++) { // 4 nibble-pairs per tile row int mx = 2 * (tile.HFlip ? 3 - xl : xl); byte pixel = tiledata[tx + yl + xl]; // Unpack nibbles and apply palette offset canvas[mx + my + r ^ 1] = (byte)((pixel & 0x0F) | palnibble); canvas[mx + my + r] = (byte)((pixel >> 4) | palnibble); } } } ``` --- ## SNES Disassembly Reference ### Do3bppToWRAM4bpp Algorithm (bank_00.asm:9759-9892) **WRAM Addresses:** - `$7E9000-$7E91FF`: Primary 4BPP conversion buffer (512 bytes) - Planes 0-3: `$7E9000 + offset` - Plane 4 (palette): `$7E9010 + offset` **Byte Layout:** ``` 3BPP Format (24 bytes per tile): Bytes 0-1: Row 0, Planes 0-1 (interleaved) Bytes 2-3: Row 1, Planes 0-1 ... Bytes 16: Row 0, Plane 2 Bytes 17: Row 1, Plane 2 ... 4BPP Format (32 bytes per tile): Bytes 0-15: Rows 0-7, Planes 0-1 (2 bytes per row) Bytes 16-31: Rows 0-7, Planes 2-3 (2 bytes per row) ``` **Conversion Pseudocode:** ```c void Convert3BppTo4Bpp(uint8_t* source_3bpp, uint8_t* wram_dest, int num_tiles) { for (int tile = 0; tile < num_tiles; tile++) { uint8_t* palette_offset = source_3bpp + 0x10; for (int word = 0; word < 4; word++) { // Read 2 bytes from 3BPP source wram_dest[0] = source_3bpp[0]; source_3bpp += 2; // Read palette plane byte wram_dest[0x10] = palette_offset[0] & 0xFF; palette_offset += 1; wram_dest += 2; } wram_dest += 0x10; // 32 bytes per 4BPP tile } } ``` --- ## Existing yaze Conversion Functions ### Available in src/app/gfx/types/snes_tile.cc **Recommended Function to Use:** ```cpp // Line 117-129: Direct BPP conversion at tile level std::vector ConvertBpp(std::span tiles, uint32_t from_bpp, uint32_t to_bpp); // Usage: std::vector converted = gfx::ConvertBpp(tiles_data, 3, 4); ``` **Alternative - Sheet Level:** ```cpp // Line 131+: Convert full graphics sheet auto sheet_8bpp = gfx::SnesTo8bppSheet(data, 3); // 3 = source BPP ``` ### WARNING: BppFormatManager Has a Bug **In src/app/gfx/util/bpp_format_manager.cc:314-318:** ```cpp std::vector BppFormatManager::Convert3BppTo8Bpp(...) { // BUG: Delegates to 4BPP conversion without actual 3BPP handling! return Convert4BppTo8Bpp(data, width, height); } ``` **Do NOT use BppFormatManager for 3BPP conversion - use snes_tile.cc functions instead.** --- ## Implementation Options ### Option A: Full 4BPP Conversion (Recommended - Matches ZScream) This is the recommended approach because it matches ZScream's working implementation and provides the clearest separation between ROM format (3BPP) and rendering format (4BPP). --- #### Step 1: Replace `Room::CopyRoomGraphicsToBuffer()` in room.cc **File**: `src/zelda3/dungeon/room.cc` **Lines to replace**: 228-295 (the entire `CopyRoomGraphicsToBuffer()` function) **Replace the ENTIRE function with this code:** ```cpp void Room::CopyRoomGraphicsToBuffer() { if (!rom_ || !rom_->is_loaded()) { printf("[CopyRoomGraphicsToBuffer] ROM not loaded\n"); return; } auto gfx_buffer_data = rom()->mutable_graphics_buffer(); if (!gfx_buffer_data || gfx_buffer_data->empty()) { printf("[CopyRoomGraphicsToBuffer] Graphics buffer is null or empty\n"); return; } printf("[CopyRoomGraphicsToBuffer] Room %d: Converting 3BPP to 4BPP\n", room_id_); // Bit extraction mask (MSB to LSB) static const uint8_t kBitMask[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; // Clear destination buffer std::fill(current_gfx16_.begin(), current_gfx16_.end(), 0); int bytes_converted = 0; int dest_pos = 0; // Process each of the 16 graphics blocks for (int block = 0; block < 16; block++) { // Validate block index if (blocks_[block] < 0 || blocks_[block] > 255) { // Skip invalid blocks, but advance destination position dest_pos += 2048; // 64 tiles * 32 bytes per 4BPP tile continue; } // Source offset in ROM graphics buffer (3BPP format) // Each 3BPP sheet is 1536 bytes (64 tiles * 24 bytes/tile) int src_sheet_offset = blocks_[block] * 1536; // Validate source bounds if (src_sheet_offset < 0 || src_sheet_offset + 1536 > static_cast(gfx_buffer_data->size())) { dest_pos += 2048; continue; } // Convert 64 tiles per block (arranged as 16x4 grid in sheet) for (int tile_row = 0; tile_row < 4; tile_row++) { // 4 rows of tiles for (int tile_col = 0; tile_col < 16; tile_col++) { // 16 tiles per row int tile_index = tile_row * 16 + tile_col; // Source offset for this tile in 3BPP format // ZScream formula: (i * 24) + (j * 384) where i=tile_col, j=tile_row int tile_src = src_sheet_offset + (tile_col * 24) + (tile_row * 384); // Convert 8 pixel rows for (int row = 0; row < 8; row++) { // Read 3 bitplanes from SNES planar format // Planes 0-1 are interleaved at bytes 0-15 // Plane 2 is at bytes 16-23 uint8_t plane0 = (*gfx_buffer_data)[tile_src + (row * 2)]; uint8_t plane1 = (*gfx_buffer_data)[tile_src + (row * 2) + 1]; uint8_t plane2 = (*gfx_buffer_data)[tile_src + 16 + row]; // Convert 8 pixels to 4 nibble-pairs (4BPP packed format) for (int nibble_pair = 0; nibble_pair < 4; nibble_pair++) { uint8_t pix1 = 0; // First pixel of pair uint8_t pix2 = 0; // Second pixel of pair // Extract first pixel color from 3 bitplanes int bit_index1 = nibble_pair * 2; if (plane0 & kBitMask[bit_index1]) pix1 |= 1; if (plane1 & kBitMask[bit_index1]) pix1 |= 2; if (plane2 & kBitMask[bit_index1]) pix1 |= 4; // Extract second pixel color from 3 bitplanes int bit_index2 = nibble_pair * 2 + 1; if (plane0 & kBitMask[bit_index2]) pix2 |= 1; if (plane1 & kBitMask[bit_index2]) pix2 |= 2; if (plane2 & kBitMask[bit_index2]) pix2 |= 4; // Pack into 4BPP format: high nibble = pix1, low nibble = pix2 // Destination uses ZScream's layout: // (row * 64) + nibble_pair + (tile_col * 4) + (tile_row * 512) + (block * 2048) int dest_index = (row * 64) + nibble_pair + (tile_col * 4) + (tile_row * 512) + (block * 2048); if (dest_index >= 0 && dest_index < static_cast(current_gfx16_.size())) { current_gfx16_[dest_index] = (pix1 << 4) | pix2; if (pix1 != 0 || pix2 != 0) bytes_converted++; } } } } } } printf("[CopyRoomGraphicsToBuffer] Room %d: Converted %d non-zero pixel pairs\n", room_id_, bytes_converted); LoadAnimatedGraphics(); } ``` --- #### Step 2: Replace `ObjectDrawer::DrawTileToBitmap()` in object_drawer.cc **File**: `src/zelda3/dungeon/object_drawer.cc` **Lines to replace**: 890-971 (the entire `DrawTileToBitmap()` function) **Replace the ENTIRE function with this code:** ```cpp void ObjectDrawer::DrawTileToBitmap(gfx::Bitmap& bitmap, const gfx::TileInfo& tile_info, int pixel_x, int pixel_y, const uint8_t* tiledata) { // Draw an 8x8 tile directly to bitmap at pixel coordinates // Graphics data is in 4BPP packed format (2 pixels per byte) if (!tiledata) return; // DEBUG: Check if bitmap is valid if (!bitmap.is_active() || bitmap.width() == 0 || bitmap.height() == 0) { LOG_DEBUG("ObjectDrawer", "ERROR: Invalid bitmap - active=%d, size=%dx%d", bitmap.is_active(), bitmap.width(), bitmap.height()); return; } // Calculate tile position in 4BPP graphics buffer // Layout: 16 tiles per row, each tile is 4 bytes wide (8 pixels / 2) // Row stride: 64 bytes (16 tiles * 4 bytes) int tile_col = tile_info.id_ % 16; int tile_row = tile_info.id_ / 16; int tile_base_x = tile_col * 4; // 4 bytes per tile horizontally int tile_base_y = tile_row * 512; // 512 bytes per tile row (8 rows * 64 bytes) // Palette offset: 4BPP uses 16 colors per palette uint8_t palette_offset = (tile_info.palette_ & 0x07) * 16; // DEBUG: Log tile info for first few tiles static int debug_tile_count = 0; if (debug_tile_count < 5) { printf("[ObjectDrawer] DrawTile4BPP: id=0x%03X pos=(%d,%d) base=(%d,%d) pal=%d\n", tile_info.id_, pixel_x, pixel_y, tile_base_x, tile_base_y, tile_info.palette_); debug_tile_count++; } // Draw 8x8 pixels (processing pixel pairs from packed bytes) int pixels_written = 0; int pixels_transparent = 0; for (int py = 0; py < 8; py++) { // Source row with vertical mirroring int src_row = tile_info.vertical_mirror_ ? (7 - py) : py; for (int nibble_pair = 0; nibble_pair < 4; nibble_pair++) { // Source column with horizontal mirroring int src_col = tile_info.horizontal_mirror_ ? (3 - nibble_pair) : nibble_pair; // Calculate source index in 4BPP buffer // ZScream layout: (row * 64) + nibble_pair + tile_base int src_index = (src_row * 64) + src_col + tile_base_x + tile_base_y; uint8_t packed_byte = tiledata[src_index]; // Unpack the two pixels from nibbles uint8_t pix1, pix2; if (tile_info.horizontal_mirror_) { // When mirrored, swap nibble order pix1 = packed_byte & 0x0F; // Low nibble first pix2 = (packed_byte >> 4) & 0x0F; // High nibble second } else { pix1 = (packed_byte >> 4) & 0x0F; // High nibble first pix2 = packed_byte & 0x0F; // Low nibble second } // Calculate destination pixel positions int px1 = nibble_pair * 2; int px2 = nibble_pair * 2 + 1; // Write first pixel if (pix1 != 0) { uint8_t final_color = pix1 + palette_offset; int dest_x = pixel_x + px1; int dest_y = pixel_y + py; if (dest_x >= 0 && dest_x < bitmap.width() && dest_y >= 0 && dest_y < bitmap.height()) { int dest_index = dest_y * bitmap.width() + dest_x; if (dest_index >= 0 && dest_index < static_cast(bitmap.mutable_data().size())) { bitmap.mutable_data()[dest_index] = final_color; pixels_written++; } } } else { pixels_transparent++; } // Write second pixel if (pix2 != 0) { uint8_t final_color = pix2 + palette_offset; int dest_x = pixel_x + px2; int dest_y = pixel_y + py; if (dest_x >= 0 && dest_x < bitmap.width() && dest_y >= 0 && dest_y < bitmap.height()) { int dest_index = dest_y * bitmap.width() + dest_x; if (dest_index >= 0 && dest_index < static_cast(bitmap.mutable_data().size())) { bitmap.mutable_data()[dest_index] = final_color; pixels_written++; } } } else { pixels_transparent++; } } } // Mark bitmap as modified if we wrote any pixels if (pixels_written > 0) { bitmap.set_modified(true); } // DEBUG: Log pixel writing stats for first few tiles if (debug_tile_count <= 5) { printf("[ObjectDrawer] Tile 0x%03X: wrote %d pixels, %d transparent\n", tile_info.id_, pixels_written, pixels_transparent); } } ``` --- #### Step 3: Verify Constants in room.h **File**: `src/zelda3/dungeon/room.h` **Line 412**: Ensure buffer size is correct ```cpp std::array current_gfx16_; // 32KB = 16 blocks * 2048 bytes ``` This is CORRECT. 32KB holds 16 blocks of 64 tiles each in 4BPP format: - 16 blocks × 64 tiles × 32 bytes/tile = 32,768 bytes = 0x8000 --- ### Option B: Keep 3BPP, Fix Palette Offset (Simpler but Less Correct) **Step 1: Change palette offset back to `* 8` in object_drawer.cc:911** ```cpp uint8_t palette_offset = (tile_info.palette_ & 0x07) * 8; // 8 colors per 3BPP palette ``` **Step 2: Ensure graphics buffer is already converted to 8BPP indexed** Check if `rom()->mutable_graphics_buffer()` already contains 8BPP indexed data (it should, based on ROM loading code). **Note**: This option is simpler but may not render correctly if the graphics buffer format doesn't match expectations. Option A is recommended. --- ## Testing Strategy ### Test Infrastructure Notes > **IMPORTANT**: The test utility functions have been updated to properly initialize the full editor system. If you're writing new GUI tests, use the provided test utilities: **Test Utilities** (defined in `test/test_utils.cc`): | Function | Purpose | |----------|---------| | `gui::LoadRomInTest(ctx, rom_path)` | Loads ROM and initializes ALL editors (calls full `LoadAssets()` flow) | | `gui::OpenEditorInTest(ctx, "Dungeon")` | Opens an editor via the **View** menu (NOT "Editors" menu!) | **Menu Structure Note**: Editors are under the `View` menu, not `Editors`: - Correct: `ctx->MenuClick("View/Dungeon")` - Incorrect: `ctx->MenuClick("Editors/Dungeon")` ← This will fail! **Full Initialization Flow**: `LoadRomInTest()` calls `Controller::LoadRomForTesting()` which: 1. Calls `EditorManager::OpenRomOrProject()` 2. Finds/creates a session for the ROM 3. Calls `ConfigureEditorDependencies()` 4. Calls `LoadAssets()` which: - Initializes all editors (registers their cards) - Loads graphics data into `gfx::Arena` - Loads dungeon/overworld/sprite data from ROM 5. Updates UI state (hides welcome screen, shows editor selection) Without this full flow, editors will appear as empty windows. --- ### Quick Build & Test Cycle ```bash # 1. Build the project cmake --build build_gemini --target yaze -j8 # 2. Run unit tests to verify no regressions ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Dungeon*:*Room*:*ObjectDrawer*" # 3. Visual test with the app ./build_gemini/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon # 4. Run specific palette test to verify fix ./build_gemini/Debug/yaze_test_stable --gtest_filter="*PaletteOffset*" ``` --- ### Unit Tests to Run After Implementation **Existing tests that MUST pass:** ```bash # Core dungeon tests ./build_gemini/Debug/yaze_test_stable --gtest_filter="DungeonObjectRenderingTests.*" ./build_gemini/Debug/yaze_test_stable --gtest_filter="DungeonPaletteTest.*" ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Room*" # All dungeon-related tests ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Dungeon*:*Object*:*Room*" ``` **Key test files:** | File | Purpose | |------|---------| | `test/integration/zelda3/dungeon_palette_test.cc` | Validates palette offset calculation | | `test/integration/zelda3/dungeon_object_rendering_tests.cc` | Tests ObjectDrawer with BackgroundBuffer | | `test/integration/zelda3/dungeon_room_test.cc` | Tests Room loading and graphics | | `test/e2e/dungeon_object_drawing_test.cc` | End-to-end drawing verification | --- ### New Test to Add: 3BPP to 4BPP Conversion Test **Create file**: `test/unit/zelda3/dungeon/bpp_conversion_test.cc` ```cpp #include #include #include namespace yaze { namespace zelda3 { namespace test { class Bpp3To4ConversionTest : public ::testing::Test { protected: // Simulates the conversion algorithm static const uint8_t kBitMask[8]; void Convert3BppTo4Bpp(const uint8_t* src_3bpp, uint8_t* dest_4bpp) { // Convert one 8x8 tile from 3BPP (24 bytes) to 4BPP packed (32 bytes) for (int row = 0; row < 8; row++) { uint8_t plane0 = src_3bpp[row * 2]; uint8_t plane1 = src_3bpp[row * 2 + 1]; uint8_t plane2 = src_3bpp[16 + row]; for (int nibble_pair = 0; nibble_pair < 4; nibble_pair++) { uint8_t pix1 = 0, pix2 = 0; int bit1 = nibble_pair * 2; int bit2 = nibble_pair * 2 + 1; if (plane0 & kBitMask[bit1]) pix1 |= 1; if (plane1 & kBitMask[bit1]) pix1 |= 2; if (plane2 & kBitMask[bit1]) pix1 |= 4; if (plane0 & kBitMask[bit2]) pix2 |= 1; if (plane1 & kBitMask[bit2]) pix2 |= 2; if (plane2 & kBitMask[bit2]) pix2 |= 4; dest_4bpp[row * 4 + nibble_pair] = (pix1 << 4) | pix2; } } } }; const uint8_t Bpp3To4ConversionTest::kBitMask[8] = { 0x80, 0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01 }; // Test that all-zero 3BPP produces all-zero 4BPP TEST_F(Bpp3To4ConversionTest, ZeroInputProducesZeroOutput) { std::array src_3bpp = {}; // All zeros std::array dest_4bpp = {}; Convert3BppTo4Bpp(src_3bpp.data(), dest_4bpp.data()); for (int i = 0; i < 32; i++) { EXPECT_EQ(dest_4bpp[i], 0) << "Byte " << i << " should be zero"; } } // Test that all-ones in plane0 produces correct pattern TEST_F(Bpp3To4ConversionTest, Plane0OnlyProducesColorIndex1) { std::array src_3bpp = {}; // Set plane0 to all 1s for first row src_3bpp[0] = 0xFF; // Row 0, plane 0 std::array dest_4bpp = {}; Convert3BppTo4Bpp(src_3bpp.data(), dest_4bpp.data()); // First row should have color index 1 for all pixels // Packed: (1 << 4) | 1 = 0x11 EXPECT_EQ(dest_4bpp[0], 0x11); EXPECT_EQ(dest_4bpp[1], 0x11); EXPECT_EQ(dest_4bpp[2], 0x11); EXPECT_EQ(dest_4bpp[3], 0x11); } // Test that all planes set produces color index 7 TEST_F(Bpp3To4ConversionTest, AllPlanesProducesColorIndex7) { std::array src_3bpp = {}; // Set all planes for first row src_3bpp[0] = 0xFF; // Row 0, plane 0 src_3bpp[1] = 0xFF; // Row 0, plane 1 src_3bpp[16] = 0xFF; // Row 0, plane 2 std::array dest_4bpp = {}; Convert3BppTo4Bpp(src_3bpp.data(), dest_4bpp.data()); // First row should have color index 7 for all pixels // Packed: (7 << 4) | 7 = 0x77 EXPECT_EQ(dest_4bpp[0], 0x77); EXPECT_EQ(dest_4bpp[1], 0x77); EXPECT_EQ(dest_4bpp[2], 0x77); EXPECT_EQ(dest_4bpp[3], 0x77); } // Test alternating pixel pattern TEST_F(Bpp3To4ConversionTest, AlternatingPixelsCorrectlyPacked) { std::array src_3bpp = {}; // Alternate: 0xAA = 10101010 (pixels 0,2,4,6 set) src_3bpp[0] = 0xAA; // Plane 0 only std::array dest_4bpp = {}; Convert3BppTo4Bpp(src_3bpp.data(), dest_4bpp.data()); // Pixels 0,2,4,6 have color 1; pixels 1,3,5,7 have color 0 // Packed: (1 << 4) | 0 = 0x10 EXPECT_EQ(dest_4bpp[0], 0x10); EXPECT_EQ(dest_4bpp[1], 0x10); EXPECT_EQ(dest_4bpp[2], 0x10); EXPECT_EQ(dest_4bpp[3], 0x10); } // Test output buffer size matches expected 4BPP format TEST_F(Bpp3To4ConversionTest, OutputSizeIs32BytesPerTile) { // 8 rows * 4 bytes per row = 32 bytes // Each row has 8 pixels, 2 pixels per byte = 4 bytes per row constexpr int kExpectedOutputSize = 32; std::array src_3bpp = {}; std::array dest_4bpp = {}; Convert3BppTo4Bpp(src_3bpp.data(), dest_4bpp.data()); // If we got here without crash, size is correct SUCCEED(); } } // namespace test } // namespace zelda3 } // namespace yaze ``` **Add to test/CMakeLists.txt:** ```cmake # Under the stable test sources, add: test/unit/zelda3/dungeon/bpp_conversion_test.cc ``` --- ### Update Existing Palette Test **File**: `test/integration/zelda3/dungeon_palette_test.cc` **Add this test to verify 4BPP conversion works end-to-end:** ```cpp TEST_F(DungeonPaletteTest, PaletteOffsetWorksWithConvertedData) { gfx::Bitmap bitmap(8, 8); bitmap.Create(8, 8, 8, std::vector(64, 0)); // Create 4BPP packed tile data (simulating converted buffer) // Layout: 512 bytes per tile row, 4 bytes per tile // For tile 0: base_x=0, base_y=0 std::vector tiledata(512 * 8, 0); // Set pixel pair at row 0: high nibble = 3, low nibble = 5 tiledata[0] = 0x35; gfx::TileInfo tile_info; tile_info.id_ = 0; tile_info.palette_ = 2; // Palette 2 → offset 32 tile_info.horizontal_mirror_ = false; tile_info.vertical_mirror_ = false; tile_info.over_ = false; drawer_->DrawTileToBitmap(bitmap, tile_info, 0, 0, tiledata.data()); const auto& data = bitmap.vector(); // Pixel 0 (high nibble 3) + offset 32 = 35 EXPECT_EQ(data[0], 35); // Pixel 1 (low nibble 5) + offset 32 = 37 EXPECT_EQ(data[1], 37); } ``` --- ### Visual Verification Checklist After implementing Option A, manually verify these scenarios: **1. Open Room 0 (Sanctuary Interior)** ```bash ./build_gemini/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon --cards="Room 0" ``` - [ ] Floor tiles render with correct brown/gray colors - [ ] Walls have proper shading gradients - [ ] No "rainbow" or garbled color patterns - [ ] Tiles align properly (no 1-pixel shifts) **2. Open Room 1 (Hyrule Castle Entrance)** - [ ] Castle wall patterns are recognizable - [ ] Door frames render correctly - [ ] Torch sconces have correct coloring **3. Open Room 263 (Ganon's Tower)** - [ ] Complex tile patterns render correctly - [ ] Multiple palette usage is visible - [ ] No missing or black tiles **4. Check All Palettes (0-7)** - Open any room and modify object palette values - [ ] Palette 0: First 16 colors work - [ ] Palette 7: Last palette range (colors 112-127) works - [ ] No overflow into adjacent palette ranges --- ### Debug Output Verification When running with the fix, you should see console output like: ``` [CopyRoomGraphicsToBuffer] Room 0: Converting 3BPP to 4BPP [CopyRoomGraphicsToBuffer] Room 0: Converted 12543 non-zero pixel pairs [ObjectDrawer] DrawTile4BPP: id=0x010 pos=(40,40) base=(0,512) pal=2 [ObjectDrawer] Tile 0x010: wrote 42 pixels, 22 transparent ``` **Good signs:** - "Converting 3BPP to 4BPP" message appears - Non-zero pixel pairs > 0 (typically 5000-15000 per room) - Tile positions (`base=`) show reasonable values - Pixels written > 0 **Bad signs:** - "Converted 0 non-zero pixel pairs" → Source data not found - All tiles show "wrote 0 pixels" → Addressing formula wrong - Crash or segfault → Buffer bounds issue --- ## Quick Verification Test (Inline Debug) **Add this debug code temporarily to verify data format:** ```cpp // In CopyRoomGraphicsToBuffer(), add after the conversion loop: printf("=== 4BPP Conversion Debug ===\n"); printf("First 32 bytes of converted buffer:\n"); for (int i = 0; i < 32; i++) { printf("%02X ", current_gfx16_[i]); if ((i + 1) % 16 == 0) printf("\n"); } printf("\nExpected: Mixed nibbles (values like 00, 11, 22, 35, 77, etc.)\n"); printf("If all zeros: Conversion failed or source data missing\n"); printf("If values > 0x77: Wrong addressing\n"); ``` --- ## File Modification Summary | File | Line | Change | |------|------|--------| | `src/zelda3/dungeon/room.cc` | 228-295 | Add 3BPP→4BPP conversion in `CopyRoomGraphicsToBuffer()` | | `src/zelda3/dungeon/object_drawer.cc` | 911 | Keep `* 16` if converting, or change to `* 8` if not | | `src/zelda3/dungeon/object_drawer.cc` | 935 | Update buffer addressing formula | | `src/zelda3/dungeon/room.h` | 412 | Keep `0x8000` buffer size (32KB is correct) | --- ## Success Criteria 1. Dungeon objects render with correct colors (not garbled/shifted) 2. Object shapes are correct (proper tile boundaries) 3. All 296 rooms load without graphical corruption 4. No performance regression (rooms should render in <100ms) 5. Palette sub-indices 0-7 map to correct colors in dungeon palette --- ## Useful Debug Commands ```bash # Run with debug logging ./build_gemini/Debug/yaze.app/Contents/MacOS/yaze --debug --log_file=debug.log --rom_file=zelda3.sfc --editor=Dungeon # Open specific room for testing ./build_gemini/Debug/yaze.app/Contents/MacOS/yaze --rom_file=zelda3.sfc --editor=Dungeon --cards="Room 0" # Run specific dungeon-related tests ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Room*:*Dungeon*:*Object*" # Run tests with verbose output ./build_gemini/Debug/yaze_test_stable --gtest_filter="*Room*" --gtest_also_run_disabled_tests ``` --- ## Troubleshooting Guide ### Issue: All tiles render as solid color or black **Cause**: Source graphics buffer offset is wrong (reading zeros or wrong data) **Debug Steps:** 1. Add debug print in `CopyRoomGraphicsToBuffer()`: ```cpp printf("Block %d: index=%d, src_offset=%d\n", block, blocks_[block], src_sheet_offset); ``` 2. Check that `blocks_[block]` values are in range 0-222 3. Verify `src_sheet_offset` doesn't exceed graphics buffer size **Fix**: The source offset calculation may need adjustment. Check if ROM graphics buffer uses different sheet sizes (some may be 2048 bytes instead of 1536). --- ### Issue: Colors are wrong but shapes are correct **Cause**: Palette offset calculation mismatch **Debug Steps:** 1. Verify palette offset in `DrawTileToBitmap()`: ```cpp printf("Palette %d -> offset %d\n", tile_info.palette_, palette_offset); ``` 2. Check expected range: palette 0-7 should give offset 0-112 **Fix**: Ensure using `* 16` for 4BPP converted data, not `* 8`. --- ### Issue: Tiles appear "scrambled" or shifted by pixels **Cause**: Buffer addressing formula is wrong **Debug Steps:** 1. For a known tile (e.g., tile ID 0), print the source indices: ```cpp printf("Tile %d: base_x=%d, base_y=%d\n", tile_info.id_, tile_base_x, tile_base_y); ``` 2. Expected for tile 0: base_x=0, base_y=0 3. Expected for tile 16: base_x=0, base_y=512 **Fix**: Check the addressing formula matches ZScream's layout: - `tile_base_x = (tile_id % 16) * 4` - `tile_base_y = (tile_id / 16) * 512` --- ### Issue: Horizontal mirroring looks wrong **Cause**: Nibble unpacking order is incorrect when mirrored **Debug Steps:** 1. Test with a known asymmetric tile 2. Check the nibble swap logic in `DrawTileToBitmap()` **Fix**: When `horizontal_mirror_` is true: - Read nibbles in reverse order from the byte - Swap which nibble goes to which pixel position --- ### Issue: Crash or segfault during rendering **Cause**: Buffer overflow - accessing memory out of bounds **Debug Steps:** 1. Check all array accesses have bounds validation 2. Add explicit bounds checks: ```cpp if (src_index >= current_gfx16_.size()) { printf("ERROR: src_index %d >= buffer size %zu\n", src_index, current_gfx16_.size()); return; } ``` **Fix**: Ensure: - `current_gfx16_` size is 0x8000 (32768 bytes) - Source index never exceeds buffer size - Destination bitmap index is within bitmap bounds --- ### Issue: Test `DungeonPaletteTest.PaletteOffsetIsCorrectFor4BPP` fails **Cause**: The test was written for old linear buffer layout **Fix**: Update the test to use the new 4BPP packed layout: ```cpp // Old test assumed linear layout: src_index = y * 128 + x // New test needs: src_index = (row * 64) + nibble_pair + tile_base ``` The test file at `test/integration/zelda3/dungeon_palette_test.cc` may need updates to match the new addressing scheme. --- ### Issue: `rom()->mutable_graphics_buffer()` returns wrong format **Cause**: ROM loading may already convert graphics to different format **Debug Steps:** 1. Check what format the graphics buffer contains: ```cpp auto gfx_buf = rom()->mutable_graphics_buffer(); printf("Graphics buffer size: %zu\n", gfx_buf->size()); printf("First 16 bytes: "); for (int i = 0; i < 16; i++) printf("%02X ", (*gfx_buf)[i]); printf("\n"); ``` 2. Compare against expected 3BPP pattern **If ROM already converts to 8BPP:** - Option A conversion is still correct (just reading from different source format) - May need to adjust source read offsets --- ### Common Constants Reference | Constant | Value | Meaning | |----------|-------|---------| | 3BPP tile size | 24 bytes | 8 rows × 3 bytes/row | | 4BPP tile size | 32 bytes | 8 rows × 4 bytes/row | | 3BPP sheet size | 1536 bytes | 64 tiles × 24 bytes | | 4BPP sheet size | 2048 bytes | 64 tiles × 32 bytes | | Tiles per row | 16 | Sheet is 16×4 tiles | | Row stride (4BPP) | 64 bytes | 16 tiles × 4 bytes | | Tile row stride | 512 bytes | 8 pixel rows × 64 bytes | | Block stride | 2048 bytes | One full 4BPP sheet | | Total buffer | 32768 bytes | 16 blocks × 2048 bytes | --- ## Stretch Goal: Cinematic GUI Test Create an interactive GUI test that visually demonstrates dungeon object rendering with deliberate pauses for observation. This test is useful for: - Verifying the fix works visually in the actual editor - Demonstrating rendering to stakeholders - Debugging rendering issues in real-time ### Create File: `test/e2e/dungeon_cinematic_rendering_test.cc` ```cpp /** * @file dungeon_cinematic_rendering_test.cc * @brief Cinematic test for watching dungeon objects render in slow-motion * * This test opens multiple dungeon rooms with deliberate pauses between * operations so you can visually observe the object rendering process. * * Run with: * ./build_gemini/Debug/yaze_test_gui --gtest_filter="*Cinematic*" * * Or register with ImGuiTestEngine for interactive execution. */ #define IMGUI_DEFINE_MATH_OPERATORS #include #include #include #include "app/controller.h" #include "rom/rom.h" #include "gtest/gtest.h" #include "imgui.h" #include "imgui_test_engine/imgui_te_context.h" #include "imgui_test_engine/imgui_te_engine.h" #include "test_utils.h" namespace yaze { namespace test { // ============================================================================= // Cinematic Test Configuration // ============================================================================= struct CinematicConfig { int frame_delay_short = 30; // ~0.5 seconds at 60fps int frame_delay_medium = 60; // ~1 second int frame_delay_long = 120; // ~2 seconds int frame_delay_dramatic = 180; // ~3 seconds (for key moments) bool log_verbose = true; }; // ============================================================================= // Room Tour Data - Interesting rooms to showcase // ============================================================================= struct RoomShowcase { int room_id; const char* name; const char* description; int view_duration; // in frames }; static const std::vector kCinematicRooms = { {0x00, "Sanctuary Interior", "Simple room - good baseline test", 120}, {0x01, "Hyrule Castle Entrance", "Complex walls and floor patterns", 150}, {0x02, "Hyrule Castle Main Hall", "Multiple layers and objects", 150}, {0x10, "Eastern Palace Entrance", "Different tileset/palette", 120}, {0x20, "Desert Palace Entrance", "Desert-themed graphics", 120}, {0x44, "Tower of Hera", "Vertical room layout", 120}, {0x60, "Skull Woods Entrance", "Dark World palette", 150}, {0x80, "Ice Palace Entrance", "Ice tileset", 120}, {0xA0, "Misery Mire Entrance", "Swamp tileset", 120}, {0xC8, "Ganon's Tower Entrance", "Complex multi-layer room", 180}, }; // ============================================================================= // Cinematic Test Functions // ============================================================================= /** * @brief Main cinematic test - tours through showcase rooms * * Opens each room with dramatic pauses, allowing visual observation of: * - Room loading animation * - Object rendering (BG1 and BG2 layers) * - Palette application * - Tile alignment */ void E2ETest_Cinematic_DungeonRoomTour(ImGuiTestContext* ctx) { CinematicConfig config; ctx->LogInfo("========================================"); ctx->LogInfo(" CINEMATIC DUNGEON RENDERING TEST"); ctx->LogInfo("========================================"); ctx->LogInfo(""); ctx->LogInfo("This test will open multiple dungeon rooms"); ctx->LogInfo("with pauses for visual observation."); ctx->LogInfo(""); ctx->Yield(config.frame_delay_dramatic); // Step 1: Load ROM ctx->LogInfo(">>> Loading ROM..."); gui::LoadRomInTest(ctx, "zelda3.sfc"); ctx->Yield(config.frame_delay_medium); ctx->LogInfo(" ROM loaded successfully!"); ctx->Yield(config.frame_delay_short); // Step 2: Open Dungeon Editor ctx->LogInfo(">>> Opening Dungeon Editor..."); gui::OpenEditorInTest(ctx, "Dungeon"); ctx->Yield(config.frame_delay_long); ctx->LogInfo(" Dungeon Editor ready!"); ctx->Yield(config.frame_delay_short); // Step 3: Enable Room Selector ctx->LogInfo(">>> Enabling Room Selector..."); if (ctx->WindowInfo("Dungeon Controls").Window != nullptr) { ctx->SetRef("Dungeon Controls"); ctx->ItemClick("Rooms"); ctx->Yield(config.frame_delay_medium); } // Step 4: Tour through rooms ctx->LogInfo(""); ctx->LogInfo("========================================"); ctx->LogInfo(" BEGINNING ROOM TOUR"); ctx->LogInfo("========================================"); ctx->Yield(config.frame_delay_medium); int rooms_visited = 0; for (const auto& room : kCinematicRooms) { ctx->LogInfo(""); ctx->LogInfo("----------------------------------------"); ctx->LogInfo("Room %d/%zu: %s (0x%02X)", rooms_visited + 1, kCinematicRooms.size(), room.name, room.room_id); ctx->LogInfo(" %s", room.description); ctx->LogInfo("----------------------------------------"); ctx->Yield(config.frame_delay_short); // Open the room char room_label[32]; snprintf(room_label, sizeof(room_label), "Room 0x%02X", room.room_id); if (ctx->WindowInfo("Room Selector").Window != nullptr) { ctx->SetRef("Room Selector"); // Try to find and click the room char search_pattern[16]; snprintf(search_pattern, sizeof(search_pattern), "[%03X]*", room.room_id); ctx->LogInfo(" >>> Opening room..."); // Scroll to room if needed ctx->ScrollToItem(search_pattern); ctx->Yield(config.frame_delay_short); // Double-click to open ctx->ItemDoubleClick(search_pattern); ctx->Yield(config.frame_delay_short); ctx->LogInfo(" >>> RENDERING IN PROGRESS..."); ctx->LogInfo(" (Watch BG1/BG2 layers draw)"); // Main viewing pause - watch the rendering ctx->Yield(room.view_duration); ctx->LogInfo(" >>> Room rendered!"); rooms_visited++; } else { ctx->LogWarning(" Room selector not available"); } ctx->Yield(config.frame_delay_short); } // Final summary ctx->LogInfo(""); ctx->LogInfo("========================================"); ctx->LogInfo(" CINEMATIC TEST COMPLETE"); ctx->LogInfo("========================================"); ctx->LogInfo(""); ctx->LogInfo("Rooms visited: %d/%zu", rooms_visited, kCinematicRooms.size()); ctx->LogInfo(""); ctx->LogInfo("Visual checks to verify:"); ctx->LogInfo(" [ ] Objects rendered with correct colors"); ctx->LogInfo(" [ ] No rainbow/garbled patterns"); ctx->LogInfo(" [ ] Tiles properly aligned (no shifts)"); ctx->LogInfo(" [ ] Different palettes visible in different rooms"); ctx->LogInfo(""); ctx->Yield(config.frame_delay_dramatic); } /** * @brief Layer toggle demonstration * * Opens a room and toggles BG1/BG2 visibility with pauses * to demonstrate layer rendering. */ void E2ETest_Cinematic_LayerToggleDemo(ImGuiTestContext* ctx) { CinematicConfig config; ctx->LogInfo("========================================"); ctx->LogInfo(" LAYER TOGGLE DEMONSTRATION"); ctx->LogInfo("========================================"); ctx->Yield(config.frame_delay_medium); // Setup gui::LoadRomInTest(ctx, "zelda3.sfc"); gui::OpenEditorInTest(ctx, "Dungeon"); ctx->Yield(config.frame_delay_medium); // Open Room 0 if (ctx->WindowInfo("Dungeon Controls").Window != nullptr) { ctx->SetRef("Dungeon Controls"); ctx->ItemClick("Rooms"); ctx->Yield(config.frame_delay_short); } if (ctx->WindowInfo("Room Selector").Window != nullptr) { ctx->SetRef("Room Selector"); ctx->ItemDoubleClick("[000]*"); ctx->Yield(config.frame_delay_long); } // Layer toggle demonstration if (ctx->WindowInfo("Room 0x00").Window != nullptr) { ctx->SetRef("Room 0x00"); ctx->LogInfo(">>> Showing both layers (default)"); ctx->Yield(config.frame_delay_long); // Toggle BG1 off if (ctx->ItemExists("Show BG1")) { ctx->LogInfo(">>> Hiding BG1 layer..."); ctx->ItemClick("Show BG1"); ctx->Yield(config.frame_delay_long); ctx->LogInfo(" (Only BG2 visible now)"); ctx->Yield(config.frame_delay_medium); // Toggle BG1 back on ctx->LogInfo(">>> Showing BG1 layer..."); ctx->ItemClick("Show BG1"); ctx->Yield(config.frame_delay_long); } // Toggle BG2 off if (ctx->ItemExists("Show BG2")) { ctx->LogInfo(">>> Hiding BG2 layer..."); ctx->ItemClick("Show BG2"); ctx->Yield(config.frame_delay_long); ctx->LogInfo(" (Only BG1 visible now)"); ctx->Yield(config.frame_delay_medium); // Toggle BG2 back on ctx->LogInfo(">>> Showing BG2 layer..."); ctx->ItemClick("Show BG2"); ctx->Yield(config.frame_delay_long); } } ctx->LogInfo("========================================"); ctx->LogInfo(" LAYER DEMO COMPLETE"); ctx->LogInfo("========================================"); } /** * @brief Palette comparison test * * Opens rooms with different palette indices side by side * to verify palette offset calculation. */ void E2ETest_Cinematic_PaletteShowcase(ImGuiTestContext* ctx) { CinematicConfig config; ctx->LogInfo("========================================"); ctx->LogInfo(" PALETTE SHOWCASE"); ctx->LogInfo("========================================"); ctx->LogInfo(""); ctx->LogInfo("Opening rooms with different palettes to verify"); ctx->LogInfo("palette offset calculation is correct."); ctx->Yield(config.frame_delay_medium); gui::LoadRomInTest(ctx, "zelda3.sfc"); gui::OpenEditorInTest(ctx, "Dungeon"); ctx->Yield(config.frame_delay_medium); // Enable room selector if (ctx->WindowInfo("Dungeon Controls").Window != nullptr) { ctx->SetRef("Dungeon Controls"); ctx->ItemClick("Rooms"); ctx->Yield(config.frame_delay_short); } // Rooms that use different palette indices struct PaletteRoom { int room_id; const char* name; int expected_palette; }; std::vector palette_rooms = { {0x00, "Sanctuary (Palette 0)", 0}, {0x01, "Hyrule Castle (Palette 1)", 1}, {0x10, "Eastern Palace (Palette 2)", 2}, {0x60, "Skull Woods (Dark Palette)", 4}, }; for (const auto& room : palette_rooms) { ctx->LogInfo(""); ctx->LogInfo(">>> %s", room.name); ctx->LogInfo(" Expected palette index: %d", room.expected_palette); ctx->LogInfo(" Expected color offset: %d", room.expected_palette * 16); if (ctx->WindowInfo("Room Selector").Window != nullptr) { ctx->SetRef("Room Selector"); char pattern[16]; snprintf(pattern, sizeof(pattern), "[%03X]*", room.room_id); ctx->ItemDoubleClick(pattern); ctx->Yield(config.frame_delay_dramatic); } } ctx->LogInfo(""); ctx->LogInfo("========================================"); ctx->LogInfo(" PALETTE SHOWCASE COMPLETE"); ctx->LogInfo("========================================"); ctx->LogInfo(""); ctx->LogInfo("Verify each room uses distinct colors!"); } // ============================================================================= // GTest Registration (for non-interactive execution) // ============================================================================= class DungeonCinematicTest : public ::testing::Test { protected: void SetUp() override { // Note: These tests require GUI mode // Skip if running in headless mode } }; TEST_F(DungeonCinematicTest, DISABLED_RoomTour) { // This test is registered with ImGuiTestEngine // Run via: ./build_gemini/Debug/yaze_test_gui --gtest_filter="*Cinematic*" GTEST_SKIP() << "Run via GUI test engine"; } TEST_F(DungeonCinematicTest, DISABLED_LayerDemo) { GTEST_SKIP() << "Run via GUI test engine"; } TEST_F(DungeonCinematicTest, DISABLED_PaletteShowcase) { GTEST_SKIP() << "Run via GUI test engine"; } } // namespace test } // namespace yaze ``` --- ### Register Tests with ImGuiTestEngine **Add to the test registration in your GUI test setup:** ```cpp // In test setup or controller initialization: if (test_engine) { ImGuiTestEngine_RegisterTest( test_engine, "Dungeon", "Cinematic_RoomTour", E2ETest_Cinematic_DungeonRoomTour); ImGuiTestEngine_RegisterTest( test_engine, "Dungeon", "Cinematic_LayerToggle", E2ETest_Cinematic_LayerToggleDemo); ImGuiTestEngine_RegisterTest( test_engine, "Dungeon", "Cinematic_PaletteShowcase", E2ETest_Cinematic_PaletteShowcase); } ``` --- ### Running the Cinematic Tests ```bash # Build with GUI tests enabled cmake --build build_gemini --target yaze_test_gui -j8 # Run all cinematic tests ./build_gemini/Debug/yaze_test_gui --gtest_filter="*Cinematic*" # Or run interactively via ImGuiTestEngine menu: # 1. Launch yaze normally # 2. Open Tools > Test Engine # 3. Select "Dungeon/Cinematic_RoomTour" # 4. Click "Run" ``` --- ### What to Watch For During the cinematic test: 1. **Room Loading Phase** - Watch the canvas area for initial rendering - Objects should appear in sequence (or all at once, depending on implementation) 2. **Color Correctness** - Browns/grays for castle walls - Distinct palettes for different dungeon types - No "rainbow" or garbled colors 3. **Layer Separation** - When BG1 is hidden, floor/background remains - When BG2 is hidden, walls/foreground remains - Both layers combine correctly when visible 4. **Tile Alignment** - No 1-pixel shifts between tiles - Object edges line up properly - No visible seams in repeated patterns --- ## Reference Documentation - `docs/internal/agents/dungeon-system-reference.md` - Full dungeon system architecture - `docs/internal/architecture/graphics_system_architecture.md` - Graphics pipeline - `CLAUDE.md` - Project coding conventions and build instructions - ZScreamDungeon source: `/Users/scawful/Code/ZScreamDungeon/ZeldaFullEditor/GraphicsManager.cs` - SNES disassembly: `assets/asm/usdasm/bank_00.asm` (lines 9759-9892)