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:
struct PaletteGroupMap {
PaletteGroup overworld_main;
PaletteGroup overworld_aux;
PaletteGroup overworld_animated;
PaletteGroup hud;
PaletteGroup global_sprites;
PaletteGroup armors;
PaletteGroup swords;
PaletteGroup shields;
PaletteGroup sprites_aux1;
PaletteGroup sprites_aux2;
PaletteGroup sprites_aux3;
PaletteGroup dungeon_main;
PaletteGroup grass;
PaletteGroup object_3d;
PaletteGroup overworld_mini_map;
};
Dungeon Palette System
Structure
- 20 dungeon palettes in the
dungeon_main
group
- 90 colors per palette (full SNES palette for BG layers)
- ROM Location:
kDungeonMainPalettes
(check snes_palette.cc
for exact address)
Usage
auto& dungeon_pal_group = rom->palette_group().dungeon_main;
int num_palettes = dungeon_pal_group.size();
int palette_id = room.palette;
auto palette = dungeon_pal_group[palette_id];
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:
if (tile_position < half_screen_width) {
tile_palette_offset = 0;
} else {
tile_palette_offset = 8;
}
Common Issues and Solutions
Issue 1: Empty Palette
Symptom: "Palette size: 0 colors" Cause: Using palette()
method instead of operator[]
Solution:
auto palette = group.palette(id);
auto palette = group[id];
Issue 2: Bitmap Corruption
Symptom: Graphics render only in top portion of image Cause: Wrong depth parameter in CreateAndRenderBitmap
Solution:
CreateAndRenderBitmap(0x200, 0x200, 0x200, data, bitmap, palette);
CreateAndRenderBitmap(0x200, 0x200, 8, data, bitmap, palette);
Issue 3: ROM Not Loaded in Preview
Symptom: "ROM not loaded" error in emulator preview Cause: Initializing before ROM is set Solution:
void Load() {
object_emulator_preview_.Initialize(rom_);
}
Palette Editor Integration
Key Functions for UI
absl::StatusOr<uint16_t> ReadColorFromRom(uint32_t address, const uint8_t* rom);
SnesColor color(snes_value);
uint8_t r = color.red();
uint8_t g = color.green();
uint8_t b = color.blue();
uint16_t snes_value = color.snes();
rom->WriteByte(address, snes_value & 0xFF);
rom->WriteByte(address + 1, (snes_value >> 8) & 0xFF);
Palette Widget Requirements
- Display: Show colors in organized grids (16 colors per row for SNES standard)
- Selection: Allow clicking to select a color
- Editing: Provide RGB sliders (0-255) or color picker
- Conversion: Auto-convert RGB (0-255) ↔ SNES (0-31) values
- Preview: Show before/after comparison
- Save: Write modified palette back to ROM
Graphics Manager Integration
Sheet Palette Assignment
if (sheet_id > 115) {
graphics_sheet.SetPaletteWithTransparent(
rom.palette_group().global_sprites[0], 0);
} else {
graphics_sheet.SetPaletteWithTransparent(
rom.palette_group().dungeon_main[0], 0);
}
Best Practices
- Always use
operator[]
for palette access - returns reference, not copy
- Validate palette IDs before accessing:
if (palette_id >= 0 && palette_id < group.size()) {
auto palette = group[palette_id];
}
- Use correct depth parameter when creating bitmaps (usually 8 for indexed color)
- Initialize ROM-dependent components only after ROM is fully loaded
- Cache palettes when repeatedly accessing the same palette
- Update textures after changing palettes (textures don't auto-update)
ROM Addresses (for reference)
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;
Graphics Sheet Palette Application
Default Palette Assignment
Graphics sheets receive default palettes during ROM loading based on their index:
if (i < 113) {
graphics_sheets[i].SetPalette(rom.palette_group().dungeon_main[0]);
} else if (i < 128) {
graphics_sheets[i].SetPalette(rom.palette_group().sprites_aux1[0]);
} else {
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:
- Apply the palette:
bitmap.SetPalette(new_palette)
- Notify Arena:
gfx::Arena::Get().NotifySheetModified(sheet_index)
- Changes propagate to all editors automatically
Common Pitfalls
Wrong Palette Access:
auto palette = group.palette(id);
auto palette = group[id];
Missing Surface Update:
bitmap.mutable_data() = new_data;
bitmap.set_data(new_data);