# SNES Palette System Overview ## Understanding SNES Color and Palette Organization ### Core Concepts #### 1. SNES Color Format (15-bit BGR555) - **Storage**: 2 bytes per color (16 bits total, 15 bits used) - **Format**: `0BBB BBGG GGGR RRRR` - Bits 0-4: Red (5 bits, 0-31) - Bits 5-9: Green (5 bits, 0-31) - Bits 10-14: Blue (5 bits, 0-31) - Bit 15: Unused (always 0) - **Range**: Each channel has 32 levels (0-31) - **Total Colors**: 32,768 possible colors (2^15) #### 2. Palette Groups in Zelda 3 Zelda 3 organizes palettes into logical groups for different game areas and entities: ```cpp struct PaletteGroupMap { PaletteGroup overworld_main; // Main overworld graphics (35 colors each) PaletteGroup overworld_aux; // Auxiliary overworld (21 colors each) PaletteGroup overworld_animated; // Animated colors (7 colors each) PaletteGroup hud; // HUD graphics (32 colors each) PaletteGroup global_sprites; // Sprite palettes (60 colors each) PaletteGroup armors; // Armor colors (15 colors each) PaletteGroup swords; // Sword colors (3 colors each) PaletteGroup shields; // Shield colors (4 colors each) PaletteGroup sprites_aux1; // Auxiliary sprite palette 1 (7 colors each) PaletteGroup sprites_aux2; // Auxiliary sprite palette 2 (7 colors each) PaletteGroup sprites_aux3; // Auxiliary sprite palette 3 (7 colors each) PaletteGroup dungeon_main; // Dungeon palettes (90 colors each) PaletteGroup grass; // Grass colors (special handling) PaletteGroup object_3d; // 3D object palettes (8 colors each) PaletteGroup overworld_mini_map; // Mini-map palettes (128 colors each) }; ``` #### 3. Color Representations in Code - **SNES 15-bit (`uint16_t`)**: On-disk format `0bbbbbgggggrrrrr`; store raw ROM words or write back with `ConvertRgbToSnes`. - **`gfx::snes_color` struct**: Expands each channel to 0-255 for arithmetic without floating point; use in converters and palette math. - **`gfx::SnesColor` class**: High-level wrapper retaining the original SNES value, a `snes_color`, and an ImVec4. Its `rgb()` accessor purposely returns 0-255 components—run the helper converters (e.g., `ConvertSnesColorToImVec4`) before handing colors to ImGui widgets that expect 0.0-1.0 floats. ### Dungeon Palette System #### Structure - **20 dungeon palettes** in the `dungeon_main` group - **90 colors per palette** (full SNES palette for BG layers) - **180 bytes per palette** (90 colors × 2 bytes per color) - **ROM Location**: `kDungeonMainPalettes = 0xDD734` #### Palette Lookup System (CRITICAL) **IMPORTANT**: Room headers store a "palette set ID" (0-71), NOT a direct palette index! The game uses a **two-level lookup system** to convert room palette properties to actual dungeon palette indices: 1. **Palette Set Table** (`paletteset_ids` at ROM `0x75460`) - 72 entries, each 4 bytes: `[bg_palette_offset, aux1, aux2, aux3]` - The first byte is a **byte offset** into the palette pointer table 2. **Palette Pointer Table** (ROM `0xDEC4B`) - Contains 16-bit words that, when divided by 180, give the palette index - Each word = ROM offset into dungeon palette data **Correct Lookup Algorithm**: ```cpp constexpr uint32_t kPalettesetIds = 0x75460; constexpr uint32_t kDungeonPalettePointerTable = 0xDEC4B; // room.palette is 0-71 (palette set ID, NOT palette index!) uint8_t byte_offset = paletteset_ids[room.palette][0]; // Step 1 uint16_t word = rom.ReadWord(kDungeonPalettePointerTable + byte_offset); // Step 2 int palette_id = word / 180; // Step 3: convert ROM offset to palette index ``` **Example Lookup**: ``` Room palette property = 16 → paletteset_ids[16][0] = 0x10 (byte offset 16) → Word at 0xDEC4B + 16 = 0x05A0 (1440) → Palette ID = 1440 / 180 = 8 → Use dungeon_main[8], NOT dungeon_main[16]! ``` **The Pointer Table (0xDEC4B)**: | Offset | Word | Palette ID | |--------|--------|------------| | 0 | 0x0000 | 0 | | 2 | 0x00B4 | 1 | | 4 | 0x0168 | 2 | | 6 | 0x021C | 3 | | ... | ... | ... | | 38 | 0x0D5C | 19 | #### Common Pitfall: Direct Palette ID Usage **WRONG** (causes purple/wrong colors for palette sets 16+): ```cpp // BUG: Uses byte offset directly as palette ID! palette_id = paletteset_ids[room.palette][0]; ``` **CORRECT**: ```cpp auto offset = paletteset_ids[room.palette][0]; auto word = rom->ReadWord(0xDEC4B + offset); palette_id = word.value() / 180; ``` #### Standard Usage ```cpp // Loading a dungeon palette (with proper lookup) auto& dungeon_pal_group = rom->palette_group().dungeon_main; int num_palettes = dungeon_pal_group.size(); // Should be 20 // Perform the two-level lookup constexpr uint32_t kDungeonPalettePointerTable = 0xDEC4B; int palette_id = room.palette; // Default fallback if (room.palette < paletteset_ids.size()) { auto offset = paletteset_ids[room.palette][0]; auto word = rom->ReadWord(kDungeonPalettePointerTable + offset); if (word.ok()) { palette_id = word.value() / 180; } } // IMPORTANT: Use operator[] not palette() method! auto palette = dungeon_pal_group[palette_id]; // Returns reference // NOT: auto palette = dungeon_pal_group.palette(palette_id); // Returns copy! ``` #### Color Distribution (90 colors) The 90 colors are typically distributed as: - **BG1 Palette** (Background Layer 1): First 8-16 subpalettes - **BG2 Palette** (Background Layer 2): Next 8-16 subpalettes - **Sprite Palettes**: Remaining colors (handled separately) Each "subpalette" is 16 colors (one SNES palette unit). ### Overworld Palette System #### Structure - **Main Overworld**: 35 colors per palette - **Auxiliary**: 21 colors per palette - **Animated**: 7 colors per palette (for water, lava effects) #### 3BPP Graphics and Left/Right Palettes Overworld graphics use 3BPP (3 bits per pixel) format: - **8 colors per tile** (2^3 = 8) - **Left Side**: Uses palette 0-7 - **Right Side**: Uses palette 8-15 When decompressing 3BPP graphics: ```cpp // Palette assignment for 3BPP overworld tiles if (tile_position < half_screen_width) { // Left side of screen tile_palette_offset = 0; // Use colors 0-7 } else { // Right side of screen tile_palette_offset = 8; // Use colors 8-15 } ``` ### Common Issues and Solutions #### Issue 1: Empty Palette **Symptom**: "Palette size: 0 colors" **Cause**: Using `palette()` method instead of `operator[]` **Solution**: ```cpp // WRONG: auto palette = group.palette(id); // Returns copy, may be empty // CORRECT: auto palette = group[id]; // Returns reference ``` #### Issue 2: Bitmap Corruption **Symptom**: Graphics render only in top portion of image **Cause**: Wrong depth parameter in `CreateAndRenderBitmap` **Solution**: ```cpp // WRONG: CreateAndRenderBitmap(0x200, 0x200, 0x200, data, bitmap, palette); // depth ^^^^ should be 8! // CORRECT: CreateAndRenderBitmap(0x200, 0x200, 8, data, bitmap, palette); // width, height, depth=8 bits ``` ### Transparency and Conversion Best Practices - Preserve ROM palette words exactly as read; hardware enforces transparency on index 0 so we no longer call `set_transparent(true)` while loading. - Apply transparency only at render time via `SetPaletteWithTransparent()` for 3BPP sub-palettes or `SetPalette()` for full 256-color assets. - `SnesColor::rgb()` yields components in 0-255 space; convert to ImGui’s expected 0.0-1.0 floats with the helper functions instead of manual divides. - Use the provided conversion helpers (`ConvertSnesToRgb`, `ImVec4ToSnesColor`, `SnesTo8bppColor`) to prevent rounding mistakes and alpha bugs. ```cpp ImVec4 rgb_255 = snes_color.rgb(); ImVec4 display = ConvertSnesColorToImVec4(snes_color); ImGui::ColorButton("color", display); ``` #### Issue 3: ROM Not Loaded in Preview **Symptom**: "ROM not loaded" error in emulator preview **Cause**: Initializing before ROM is set **Solution**: ```cpp // Initialize emulator preview AFTER ROM is loaded and set void Load() { // ... load ROM data ... // ... set up other components ... // NOW initialize emulator preview with loaded ROM object_emulator_preview_.Initialize(rom_); } ``` ### Palette Editor Integration #### Key Functions for UI ```cpp // Reading a color from ROM absl::StatusOr ReadColorFromRom(uint32_t address, const uint8_t* rom); // Converting SNES color to RGB SnesColor color(snes_value); // snes_value is uint16_t uint8_t r = color.red(); // 0-255 (converted from 0-31) uint8_t g = color.green(); // 0-255 uint8_t b = color.blue(); // 0-255 // Writing color back to ROM uint16_t snes_value = color.snes(); // Get 15-bit BGR555 value rom->WriteByte(address, snes_value & 0xFF); // Low byte rom->WriteByte(address + 1, (snes_value >> 8) & 0xFF); // High byte ``` #### Palette Widget Requirements 1. **Display**: Show colors in organized grids (16 colors per row for SNES standard) 2. **Selection**: Allow clicking to select a color 3. **Editing**: Provide RGB sliders (0-255) or color picker 4. **Conversion**: Auto-convert RGB (0-255) ↔ SNES (0-31) values 5. **Preview**: Show before/after comparison 6. **Save**: Write modified palette back to ROM #### Palette UI Helpers - `InlinePaletteSelector` renders a lightweight selection strip (no editing) ideal for 8- or 16-color sub-palettes. - `InlinePaletteEditor` supplies the full editing experience with ImGui color pickers, context menus, and optional live preview toggles. - `PopupPaletteEditor` fits in context menus or modals; it caps at 64 colors to keep popups manageable. - Legacy helpers such as `DisplayPalette()` remain for backward compatibility but inherit the 32-color limit—prefer the new helpers for new UI. -### Metadata-Driven Palette Application `gfx::BitmapMetadata` tracks the source BPP, palette format, type string, and expected color count. Set it immediately after creating a bitmap so later code can make the right choice automatically: ```cpp bitmap.metadata() = BitmapMetadata{/*source_bpp=*/3, /*palette_format=*/1, // 0=full, 1=sub-palette /*source_type=*/"graphics_sheet", /*palette_colors=*/8}; bitmap.ApplyPaletteByMetadata(palette); ``` - `palette_format == 0` routes to `SetPalette()` and preserves every color (Mode 7, HUD assets, etc.). - `palette_format == 1` routes to `SetPaletteWithTransparent()` and injects the transparent color 0 for 3BPP workflows. - Validation hooks help catch mismatched palette sizes before they hit SDL. ### Graphics Manager Integration #### Sheet Palette Assignment ```cpp // Assigning palette to graphics sheet if (sheet_id > 115) { // Sprite sheets use sprite palette graphics_sheet.SetPaletteWithTransparent( rom.palette_group().global_sprites[0], 0); } else { // Dungeon sheets use dungeon palette graphics_sheet.SetPaletteWithTransparent( rom.palette_group().dungeon_main[0], 0); } ``` ### Texture Synchronization and Regression Notes - Call `bitmap.UpdateSurfacePixels()` after mutating `bitmap.mutable_data()` to copy rendered bytes into the SDL surface before queuing texture creation or updates. - `Bitmap::ApplyStoredPalette()` now rebuilds an `SDL_Color` array sized to the actual palette instead of forcing 256 entries—this fixes regressions where 8- or 16-color palettes were padded with opaque black. - When updating SDL palette data yourself, mirror that pattern: ```cpp std::vector colors(palette.size()); for (size_t i = 0; i < palette.size(); ++i) { const auto& c = palette[i]; const ImVec4 rgb = c.rgb(); // 0-255 components colors[i] = SDL_Color{static_cast(rgb.x), static_cast(rgb.y), static_cast(rgb.z), c.is_transparent() ? 0 : 255}; } SDL_SetPaletteColors(surface->format->palette, colors.data(), 0, static_cast(colors.size())); ``` ### Best Practices 1. **Always use `operator[]` for palette access** - returns reference, not copy 2. **Validate palette IDs** before accessing: ```cpp if (palette_id >= 0 && palette_id < group.size()) { auto palette = group[palette_id]; } ``` 3. **Use correct depth parameter** when creating bitmaps (usually 8 for indexed color) 4. **Initialize ROM-dependent components** only after ROM is fully loaded 5. **Cache palettes** when repeatedly accessing the same palette 6. **Update textures** after changing palettes (textures don't auto-update) ### User Workflow Tips - Choose the widget that matches the task: selectors for choosing colors, editors for full control, popups for contextual tweaks. - The live preview toggle trades responsiveness for performance; disable it while batch-editing large (64+ color) palettes. - Right-click any swatch in the editor to copy the color as SNES hex, RGB tuples, or HTML hex—useful when coordinating with external art tools. - Remember hardware rules: palette index 0 is always transparent and will not display even if the stored value is non-zero. - Keep ROM backups when performing large palette sweeps; palette groups are shared across screens so a change can have multiple downstream effects. ### ROM Addresses (for reference) ```cpp // From snes_palette.cc constexpr uint32_t kOverworldPaletteMain = 0xDE6C8; constexpr uint32_t kOverworldPaletteAux = 0xDE86C; constexpr uint32_t kOverworldPaletteAnimated = 0xDE604; constexpr uint32_t kHudPalettes = 0xDD218; constexpr uint32_t kGlobalSpritesLW = 0xDD308; constexpr uint32_t kArmorPalettes = 0xDD630; constexpr uint32_t kSwordPalettes = 0xDD630; constexpr uint32_t kShieldPalettes = 0xDD648; constexpr uint32_t kSpritesPalettesAux1 = 0xDD39E; constexpr uint32_t kSpritesPalettesAux2 = 0xDD446; constexpr uint32_t kSpritesPalettesAux3 = 0xDD4E0; constexpr uint32_t kDungeonMainPalettes = 0xDD734; constexpr uint32_t kHardcodedGrassLW = 0x5FEA9; constexpr uint32_t kTriforcePalette = 0xF4CD0; constexpr uint32_t kOverworldMiniMapPalettes = 0x55B27; // Dungeon palette lookup tables (critical for room rendering!) constexpr uint32_t kPalettesetIds = 0x75460; // 72 entries × 4 bytes constexpr uint32_t kDungeonPalettePointerTable = 0xDEC4B; // Palette ROM offsets ``` ## Graphics Sheet Palette Application ### Default Palette Assignment Graphics sheets receive default palettes during ROM loading based on their index: ```cpp // In LoadAllGraphicsData() - rom.cc if (i < 113) { // Sheets 0-112: Overworld/Dungeon graphics graphics_sheets[i].SetPalette(rom.palette_group().dungeon_main[0]); } else if (i < 128) { // Sheets 113-127: Sprite graphics graphics_sheets[i].SetPalette(rom.palette_group().sprites_aux1[0]); } else { // Sheets 128-222: Auxiliary/HUD graphics graphics_sheets[i].SetPalette(rom.palette_group().hud.palette(0)); } ``` This ensures graphics are visible immediately after loading rather than appearing white. ### Palette Update Workflow When changing a palette in any editor: 1. Apply the palette: `bitmap.SetPalette(new_palette)` 2. Notify Arena: `gfx::Arena::Get().NotifySheetModified(sheet_index)` 3. Changes propagate to all editors automatically ### Common Pitfalls **Wrong Palette Access**: ```cpp // WRONG - Returns copy, may be empty auto palette = group.palette(id); // CORRECT - Returns reference auto palette = group[id]; ``` **Missing Surface Update**: ```cpp // WRONG - Only updates vector, not SDL surface bitmap.mutable_data() = new_data; // CORRECT - Updates both vector and surface bitmap.set_data(new_data); ``` ## Bitmap Dual Palette System ### Understanding the Two Palette Storage Mechanisms The `Bitmap` class has **two separate palette storage locations**, which can cause confusion: | Storage | Location | Populated By | Used For | |---------|----------|--------------|----------| | Internal SnesPalette | `bitmap.palette_` | `SetPalette(SnesPalette)` | Serialization, palette editing | | SDL Surface Palette | `surface_->format->palette` | Both `SetPalette` overloads | Actual rendering to textures | ### The Problem: Empty palette() Returns When dungeon rooms apply palettes to their layer buffers, they use `SetPalette(vector)`: ```cpp // In room.cc - CreateAllGraphicsLayers() auto set_dungeon_palette = [](gfx::Bitmap& bmp, const gfx::SnesPalette& pal) { std::vector colors(256); for (size_t i = 0; i < pal.size() && i < 256; ++i) { ImVec4 rgb = pal[i].rgb(); colors[i] = { static_cast(rgb.x), static_cast(rgb.y), static_cast(rgb.z), 255 }; } colors[255] = {0, 0, 0, 0}; // Transparent bmp.SetPalette(colors); // Uses SDL_Color overload! }; ``` This means `bitmap.palette().size()` returns **0** even though the bitmap renders correctly! ### Solution: Extract Palette from SDL Surface When you need to copy a palette between bitmaps (e.g., for layer compositing), extract it from the SDL surface: ```cpp void CopyPaletteBetweenBitmaps(const gfx::Bitmap& src, gfx::Bitmap& dst) { SDL_Surface* src_surface = src.surface(); if (!src_surface || !src_surface->format) return; SDL_Palette* src_pal = src_surface->format->palette; if (!src_pal || src_pal->ncolors == 0) return; // Extract palette colors into a vector std::vector colors(256); int colors_to_copy = std::min(src_pal->ncolors, 256); for (int i = 0; i < colors_to_copy; ++i) { colors[i] = src_pal->colors[i]; } // Apply to destination bitmap dst.SetPalette(colors); } ``` ### Layer Compositing with Correct Palettes When merging multiple layers into a single composite bitmap (as done in `RoomLayerManager::CompositeToOutput()`), the correct approach is: 1. Create/clear the output bitmap 2. For each visible layer: - Extract the SDL palette from the first layer with a valid surface - Apply it to the output bitmap using `SetPalette(vector)` - Composite the pixel data (skip transparent indices 0 and 255) 3. Sync pixel data to surface with `UpdateSurfacePixels()` 4. Mark as modified for texture update **Example from RoomLayerManager**: ```cpp void RoomLayerManager::CompositeToOutput(Room& room, gfx::Bitmap& output) const { // Create output bitmap output.Create(512, 512, 8, std::vector(512*512, 255)); bool palette_copied = false; for (auto layer_type : GetDrawOrder()) { auto& buffer = GetLayerBuffer(room, layer_type); const auto& src_bitmap = buffer.bitmap(); // Copy palette from first visible layer if (!palette_copied && src_bitmap.surface()) { ApplySDLPaletteToBitmap(src_bitmap.surface(), output); palette_copied = true; } // Composite pixels... } output.UpdateSurfacePixels(); output.set_modified(true); } ``` ### Best Practices for Palette Handling 1. **Don't assume palette() has data**: Always check `palette().size() > 0` before using it 2. **Use SDL surface as authoritative source**: For rendering-related palette operations 3. **Use SetPalette(SnesPalette) for persistence**: When the palette needs to be saved or edited 4. **Use SetPalette(vector) for performance**: When you already have SDL colors 5. **Always call UpdateSurfacePixels()**: After modifying pixel data and before rendering