Files
yaze/docs/public/developer/palette-system-overview.md

529 lines
19 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
index0 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 ImGuis
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<uint16_t> 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
(Mode7, 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<SDL_Color> 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<Uint8>(rgb.x),
static_cast<Uint8>(rgb.y),
static_cast<Uint8>(rgb.z),
c.is_transparent() ? 0 : 255};
}
SDL_SetPaletteColors(surface->format->palette, colors.data(), 0,
static_cast<int>(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 index0 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<SDL_Color>)`:
```cpp
// In room.cc - CreateAllGraphicsLayers()
auto set_dungeon_palette = [](gfx::Bitmap& bmp, const gfx::SnesPalette& pal) {
std::vector<SDL_Color> colors(256);
for (size_t i = 0; i < pal.size() && i < 256; ++i) {
ImVec4 rgb = pal[i].rgb();
colors[i] = { static_cast<Uint8>(rgb.x), static_cast<Uint8>(rgb.y),
static_cast<Uint8>(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<SDL_Color> 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<SDL_Color>)`
- 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<uint8_t>(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<SDL_Color>) for performance**: When you already have SDL colors
5. **Always call UpdateSurfacePixels()**: After modifying pixel data and before rendering