feat(gfx): enhance Bitmap class with palette management and metadata support
- Added methods for applying palettes based on metadata, allowing for flexible palette handling in different bitmap types. - Introduced a new BitmapMetadata struct to track source format and palette requirements. - Enhanced ApplyStoredPalette and SetPaletteWithTransparent methods for improved palette application and transparency handling. - Updated SDL surface pixel management with a new UpdateSurfacePixels method for better pixel data handling. Benefits: - Improves the rendering capabilities of the Bitmap class by supporting various palette formats. - Enhances user experience with more intuitive palette management in graphics operations.
This commit is contained in:
@@ -249,6 +249,22 @@ void Bitmap::UpdateTexture() {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Apply the stored palette to the SDL surface
|
||||
*
|
||||
* This method applies the palette_ member to the SDL surface's palette.
|
||||
*
|
||||
* IMPORTANT: Transparency handling
|
||||
* - ROM palette data does NOT have transparency flags set
|
||||
* - Transparency is only applied if explicitly marked (via set_transparent)
|
||||
* - For SNES rendering, use SetPaletteWithTransparent which creates
|
||||
* transparent color 0 automatically
|
||||
* - This method preserves the transparency state of each color
|
||||
*
|
||||
* Color format notes:
|
||||
* - SnesColor.rgb() returns 0-255 values stored in ImVec4 (unconventional!)
|
||||
* - We cast these directly to Uint8 for SDL
|
||||
*/
|
||||
void Bitmap::ApplyStoredPalette() {
|
||||
if (surface_ == nullptr) {
|
||||
return; // Can't apply without surface
|
||||
@@ -273,12 +289,16 @@ void Bitmap::ApplyStoredPalette() {
|
||||
// Apply all palette colors from the SnesPalette
|
||||
for (size_t i = 0; i < palette_.size() && i < 256; ++i) {
|
||||
const auto& pal_color = palette_[i];
|
||||
// NOTE: rgb() stores 0-255 values directly in ImVec4 (unconventional but intentional)
|
||||
sdl_palette->colors[i].r = static_cast<Uint8>(pal_color.rgb().x);
|
||||
sdl_palette->colors[i].g = static_cast<Uint8>(pal_color.rgb().y);
|
||||
sdl_palette->colors[i].b = static_cast<Uint8>(pal_color.rgb().z);
|
||||
|
||||
// CRITICAL: Transparency for color 0 of each sub-palette
|
||||
// Get RGB values - stored as 0-255 in ImVec4 (unconventional!)
|
||||
ImVec4 rgb_255 = pal_color.rgb();
|
||||
|
||||
sdl_palette->colors[i].r = static_cast<Uint8>(rgb_255.x);
|
||||
sdl_palette->colors[i].g = static_cast<Uint8>(rgb_255.y);
|
||||
sdl_palette->colors[i].b = static_cast<Uint8>(rgb_255.z);
|
||||
|
||||
// Only apply transparency if explicitly set
|
||||
// (ROM data won't have this set; transparency is added during rendering)
|
||||
if (pal_color.is_transparent()) {
|
||||
sdl_palette->colors[i].a = 0; // Fully transparent
|
||||
} else {
|
||||
@@ -300,9 +320,67 @@ void Bitmap::SetPalette(const SnesPalette &palette) {
|
||||
modified_ = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply palette using metadata-driven strategy
|
||||
*
|
||||
* Uses bitmap metadata to determine the appropriate palette application method:
|
||||
* - palette_format == 0: Full palette (SetPalette)
|
||||
* - palette_format == 1: Sub-palette with transparent color 0 (SetPaletteWithTransparent)
|
||||
*
|
||||
* This ensures correct rendering for different bitmap types:
|
||||
* - 3BPP graphics sheets → sub-palette with transparent
|
||||
* - 4BPP full palettes → full palette
|
||||
* - Mode 7 graphics → full palette
|
||||
*
|
||||
* @param palette Source palette to apply
|
||||
* @param sub_palette_index Index within palette for sub-palette extraction (default 0)
|
||||
*/
|
||||
void Bitmap::ApplyPaletteByMetadata(const SnesPalette& palette, int sub_palette_index) {
|
||||
if (metadata_.palette_format == 1) {
|
||||
// Sub-palette: need transparent black + 7 colors from palette
|
||||
// Common for 3BPP graphics sheets (title screen, etc.)
|
||||
SetPaletteWithTransparent(palette, sub_palette_index, 7);
|
||||
} else {
|
||||
// Full palette application
|
||||
// Used for 4BPP, Mode 7, and other full-color formats
|
||||
SetPalette(palette);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Apply a sub-palette with automatic transparency for SNES rendering
|
||||
*
|
||||
* This method extracts a sub-palette from a larger palette and applies it
|
||||
* to the SDL surface with proper SNES transparency handling.
|
||||
*
|
||||
* SNES Transparency Model:
|
||||
* - The SNES hardware automatically treats palette index 0 as transparent
|
||||
* - This is a hardware feature, not stored in ROM data
|
||||
* - This method creates a transparent color 0 for proper SNES emulation
|
||||
*
|
||||
* Usage:
|
||||
* - Extract 8-color sub-palette from position 'index' in source palette
|
||||
* - Color 0: Always set to transparent black (0,0,0,0)
|
||||
* - Colors 1-7: Taken from palette[index] through palette[index+6]
|
||||
* - If palette has fewer than 7 colors, fills with opaque black
|
||||
*
|
||||
* Example:
|
||||
* palette has colors [c0, c1, c2, c3, c4, c5, c6, c7, c8, ...]
|
||||
* SetPaletteWithTransparent(palette, 0, 7) creates:
|
||||
* [transparent_black, c0, c1, c2, c3, c4, c5, c6]
|
||||
*
|
||||
* IMPORTANT: Source palette data is NOT modified
|
||||
* - The full palette is stored in palette_ member for reference
|
||||
* - Only the SDL surface palette is updated with the 8-color subset
|
||||
* - This allows proper palette editing while maintaining SNES rendering
|
||||
*
|
||||
* @param palette Source palette (can be 7, 8, 64, 128, or 256 colors)
|
||||
* @param index Start index in source palette (0-based)
|
||||
* @param length Number of colors to extract (default 7, max 7)
|
||||
*/
|
||||
void Bitmap::SetPaletteWithTransparent(const SnesPalette &palette, size_t index,
|
||||
int length) {
|
||||
// Store palette even if surface isn't ready yet
|
||||
// Store the full palette for reference (not modified)
|
||||
palette_ = palette;
|
||||
|
||||
// If surface isn't created yet, just store the palette for later
|
||||
@@ -310,54 +388,52 @@ void Bitmap::SetPaletteWithTransparent(const SnesPalette &palette, size_t index,
|
||||
return; // Palette will be applied when surface is created
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Use index directly as palette slot, not index * 7
|
||||
// For 8-color palettes, index should be 0-7, not 0-49
|
||||
// Validate parameters
|
||||
if (index >= palette.size()) {
|
||||
throw std::invalid_argument("Invalid palette index");
|
||||
}
|
||||
|
||||
if (length < 0 || length > 8) {
|
||||
throw std::invalid_argument("Invalid palette length (must be 0-8 for 8-color palettes)");
|
||||
if (length < 0 || length > 7) {
|
||||
throw std::invalid_argument("Invalid palette length (must be 0-7 for SNES palettes)");
|
||||
}
|
||||
|
||||
if (index + length > palette.size()) {
|
||||
throw std::invalid_argument("Palette index + length exceeds size");
|
||||
}
|
||||
|
||||
// Extract 8-color sub-palette starting at the specified index
|
||||
// This correctly handles both 256-color overworld palettes and smaller palettes
|
||||
// Build 8-color SNES sub-palette
|
||||
std::vector<ImVec4> colors;
|
||||
|
||||
// Always start with transparent color (index 0)
|
||||
colors.push_back(ImVec4(0, 0, 0, 0));
|
||||
// Color 0: Transparent (SNES hardware requirement)
|
||||
colors.push_back(ImVec4(0, 0, 0, 0)); // Transparent black
|
||||
|
||||
// Extract up to 7 colors from the palette starting at index
|
||||
// Colors 1-7: Extract from source palette
|
||||
// NOTE: palette[i].rgb() returns 0-255 values in ImVec4 (unconventional!)
|
||||
for (size_t i = 0; i < 7 && (index + i) < palette.size(); ++i) {
|
||||
auto &pal_color = palette[index + i];
|
||||
colors.push_back(pal_color.rgb());
|
||||
const auto &pal_color = palette[index + i];
|
||||
ImVec4 rgb_255 = pal_color.rgb(); // 0-255 range (unconventional storage)
|
||||
|
||||
// Convert to standard ImVec4 0-1 range for SDL
|
||||
colors.push_back(ImVec4(rgb_255.x / 255.0f, rgb_255.y / 255.0f,
|
||||
rgb_255.z / 255.0f, 1.0f)); // Always opaque
|
||||
}
|
||||
|
||||
// Ensure we have exactly 8 colors (transparent + 7 data colors)
|
||||
// Ensure we have exactly 8 colors
|
||||
while (colors.size() < 8) {
|
||||
colors.push_back(ImVec4(0, 0, 0, 1.0f)); // Fill with black if needed
|
||||
colors.push_back(ImVec4(0, 0, 0, 1.0f)); // Fill with opaque black
|
||||
}
|
||||
|
||||
// CRITICAL FIX: Keep the original complete palette in palette_ member
|
||||
// Only update the SDL surface palette for display purposes
|
||||
// This prevents breaking other editors that expect the complete palette
|
||||
if (palette_.size() != palette.size()) {
|
||||
palette_ = palette; // Store complete palette
|
||||
InvalidatePaletteCache(); // Update cache with complete palette
|
||||
}
|
||||
// Update palette cache with full palette (for color lookup)
|
||||
InvalidatePaletteCache();
|
||||
|
||||
// Apply the 8-color sub-palette to SDL surface for display
|
||||
// Apply the 8-color SNES sub-palette to SDL surface
|
||||
SDL_UnlockSurface(surface_);
|
||||
for (int color_index = 0; color_index < 8 && color_index < static_cast<int>(colors.size()); ++color_index) {
|
||||
if (color_index < surface_->format->palette->ncolors) {
|
||||
surface_->format->palette->colors[color_index].r = static_cast<Uint8>(colors[color_index].x * 255);
|
||||
surface_->format->palette->colors[color_index].g = static_cast<Uint8>(colors[color_index].y * 255);
|
||||
surface_->format->palette->colors[color_index].b = static_cast<Uint8>(colors[color_index].z * 255);
|
||||
surface_->format->palette->colors[color_index].a = static_cast<Uint8>(colors[color_index].w * 255);
|
||||
surface_->format->palette->colors[color_index].r = static_cast<Uint8>(colors[color_index].x * 255.0f);
|
||||
surface_->format->palette->colors[color_index].g = static_cast<Uint8>(colors[color_index].y * 255.0f);
|
||||
surface_->format->palette->colors[color_index].b = static_cast<Uint8>(colors[color_index].z * 255.0f);
|
||||
surface_->format->palette->colors[color_index].a = static_cast<Uint8>(colors[color_index].w * 255.0f);
|
||||
}
|
||||
}
|
||||
SDL_LockSurface(surface_);
|
||||
|
||||
@@ -167,6 +167,12 @@ class Bitmap {
|
||||
void SetPaletteWithTransparent(const SnesPalette &palette, size_t index,
|
||||
int length = 7);
|
||||
|
||||
/**
|
||||
* @brief Apply palette using metadata-driven strategy
|
||||
* Chooses between SetPalette and SetPaletteWithTransparent based on metadata
|
||||
*/
|
||||
void ApplyPaletteByMetadata(const SnesPalette& palette, int sub_palette_index = 0);
|
||||
|
||||
/**
|
||||
* @brief Apply the stored palette to the surface (internal helper)
|
||||
*/
|
||||
@@ -248,8 +254,25 @@ class Bitmap {
|
||||
void Get16x16Tile(int tile_x, int tile_y, std::vector<uint8_t> &tile_data,
|
||||
int &tile_data_offset);
|
||||
|
||||
/**
|
||||
* @brief Metadata for tracking bitmap source format and palette requirements
|
||||
*/
|
||||
struct BitmapMetadata {
|
||||
int source_bpp = 8; // Original bits per pixel (3, 4, 8)
|
||||
int palette_format = 0; // 0=full palette, 1=sub-palette with transparent
|
||||
std::string source_type; // "graphics_sheet", "tilemap", "screen_buffer", "mode7"
|
||||
int palette_colors = 256; // Expected palette size
|
||||
|
||||
BitmapMetadata() = default;
|
||||
BitmapMetadata(int bpp, int format, const std::string& type, int colors = 256)
|
||||
: source_bpp(bpp), palette_format(format), source_type(type), palette_colors(colors) {}
|
||||
};
|
||||
|
||||
const SnesPalette &palette() const { return palette_; }
|
||||
SnesPalette *mutable_palette() { return &palette_; }
|
||||
BitmapMetadata& metadata() { return metadata_; }
|
||||
const BitmapMetadata& metadata() const { return metadata_; }
|
||||
|
||||
int width() const { return width_; }
|
||||
int height() const { return height_; }
|
||||
int depth() const { return depth_; }
|
||||
@@ -285,6 +308,9 @@ class Bitmap {
|
||||
// Palette for the bitmap
|
||||
gfx::SnesPalette palette_;
|
||||
|
||||
// Metadata for tracking source format and palette requirements
|
||||
BitmapMetadata metadata_;
|
||||
|
||||
// Data for the bitmap
|
||||
std::vector<uint8_t> data_;
|
||||
|
||||
|
||||
@@ -99,14 +99,14 @@ std::vector<SnesColor> GetColFileData(uint8_t* data) {
|
||||
}
|
||||
|
||||
void SnesColor::set_rgb(const ImVec4 val) {
|
||||
// ImGui ColorPicker returns colors in 0-1 range, but internally we store 0-255
|
||||
// Convert from 0-1 normalized to 0-255 range
|
||||
// IMPORTANT: Input val is expected to be in standard ImVec4 range (0.0-1.0)
|
||||
// We convert to internal 0-255 storage format
|
||||
rgb_.x = val.x * kColorByteMax;
|
||||
rgb_.y = val.y * kColorByteMax;
|
||||
rgb_.z = val.z * kColorByteMax;
|
||||
rgb_.w = kColorByteMaxF; // Alpha always 255
|
||||
|
||||
// Create snes_color struct for ROM/SNES conversion (expects 0-255 range)
|
||||
// Update rom_color_ and snes_ representations
|
||||
snes_color color;
|
||||
color.red = static_cast<uint16_t>(rgb_.x);
|
||||
color.green = static_cast<uint16_t>(rgb_.y);
|
||||
@@ -118,10 +118,18 @@ void SnesColor::set_rgb(const ImVec4 val) {
|
||||
}
|
||||
|
||||
void SnesColor::set_snes(uint16_t val) {
|
||||
// Store SNES 15-bit color
|
||||
snes_ = val;
|
||||
|
||||
// Convert SNES to RGB (0-255)
|
||||
snes_color col = ConvertSnesToRgb(val);
|
||||
// ConvertSnesToRgb returns 0-255 range, store directly (not normalized 0-1)
|
||||
rgb_ = ImVec4(col.red, col.green, col.blue, kColorByteMaxF);
|
||||
|
||||
// Store 0-255 values in ImVec4 (unconventional but our internal format)
|
||||
rgb_ = ImVec4(static_cast<float>(col.red),
|
||||
static_cast<float>(col.green),
|
||||
static_cast<float>(col.blue),
|
||||
kColorByteMaxF);
|
||||
|
||||
rom_color_ = col;
|
||||
modified = true;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,75 @@ namespace gfx {
|
||||
|
||||
constexpr int NumberOfColors = 3143;
|
||||
|
||||
// ============================================================================
|
||||
// SNES Color Conversion Functions
|
||||
// ============================================================================
|
||||
//
|
||||
// Color Format Guide:
|
||||
// - SNES Color (uint16_t): 15-bit BGR format (0bbbbbgggggrrrrr)
|
||||
// - snes_color struct: RGB values in 0-255 range
|
||||
// - ImVec4: RGBA values in 0.0-1.0 range (standard for ImGui)
|
||||
// - SDL_Color: RGBA values in 0-255 range
|
||||
//
|
||||
// Conversion paths:
|
||||
// 1. SNES (uint16_t) <-> snes_color (0-255) <-> ImVec4 (0.0-1.0)
|
||||
// 2. Use these functions to convert between formats explicitly
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Convert SNES 15-bit color to RGB (0-255 range)
|
||||
* @param snes_color SNES color in 15-bit BGR format (0bbbbbgggggrrrrr)
|
||||
* @return snes_color struct with RGB values in 0-255 range
|
||||
*/
|
||||
snes_color ConvertSnesToRgb(uint16_t snes_color);
|
||||
|
||||
/**
|
||||
* @brief Convert RGB (0-255) to SNES 15-bit color
|
||||
* @param color snes_color struct with RGB values in 0-255 range
|
||||
* @return SNES color in 15-bit BGR format
|
||||
*/
|
||||
uint16_t ConvertRgbToSnes(const snes_color& color);
|
||||
|
||||
/**
|
||||
* @brief Convert ImVec4 (0.0-1.0) to SNES 15-bit color
|
||||
* @param color ImVec4 with RGB values in 0.0-1.0 range
|
||||
* @return SNES color in 15-bit BGR format
|
||||
*/
|
||||
uint16_t ConvertRgbToSnes(const ImVec4& color);
|
||||
|
||||
/**
|
||||
* @brief Convert snes_color (0-255) to ImVec4 (0.0-1.0)
|
||||
* @param color snes_color struct with RGB values in 0-255 range
|
||||
* @return ImVec4 with RGBA values in 0.0-1.0 range
|
||||
*/
|
||||
inline ImVec4 SnesColorToImVec4(const snes_color& color) {
|
||||
return ImVec4(color.red / 255.0f, color.green / 255.0f,
|
||||
color.blue / 255.0f, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert ImVec4 (0.0-1.0) to snes_color (0-255)
|
||||
* @param color ImVec4 with RGB values in 0.0-1.0 range
|
||||
* @return snes_color struct with RGB values in 0-255 range
|
||||
*/
|
||||
inline snes_color ImVec4ToSnesColor(const ImVec4& color) {
|
||||
snes_color result;
|
||||
result.red = static_cast<uint16_t>(color.x * 255.0f);
|
||||
result.green = static_cast<uint16_t>(color.y * 255.0f);
|
||||
result.blue = static_cast<uint16_t>(color.z * 255.0f);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert SNES 15-bit color directly to ImVec4 (0.0-1.0)
|
||||
* @param color_value SNES color in 15-bit BGR format
|
||||
* @return ImVec4 with RGBA values in 0.0-1.0 range
|
||||
*/
|
||||
inline ImVec4 SnesTo8bppColor(uint16_t color_value) {
|
||||
snes_color rgb = ConvertSnesToRgb(color_value);
|
||||
return SnesColorToImVec4(rgb);
|
||||
}
|
||||
|
||||
std::vector<snes_color> Extract(const char* data, unsigned int offset,
|
||||
unsigned int palette_size);
|
||||
|
||||
@@ -28,20 +93,30 @@ constexpr float kColorByteMaxF = 255.f;
|
||||
/**
|
||||
* @brief SNES Color container
|
||||
*
|
||||
* Used for displaying the color to the screen and writing
|
||||
* the color to the Rom file in the correct format.
|
||||
* Manages SNES colors in multiple formats for editing and display.
|
||||
*
|
||||
* SNES colors may be represented in one of three formats:
|
||||
* - Color data from the rom in a snes_color struct
|
||||
* - Color data for displaying to the UI via ImVec4
|
||||
* IMPORTANT: Internal storage format
|
||||
* - rgb_: ImVec4 storing RGB values in 0-255 range (NOT standard 0-1!)
|
||||
* This is unconventional but done for performance reasons
|
||||
* - snes_: SNES 15-bit BGR format (0bbbbbgggggrrrrr)
|
||||
* - rom_color_: snes_color struct with 0-255 RGB values
|
||||
*
|
||||
* When getting RGB for display:
|
||||
* - Use rgb() to get raw values (0-255 in ImVec4 - unusual!)
|
||||
* - Convert to standard ImVec4 (0-1) using: ImVec4(rgb.x/255, rgb.y/255, rgb.z/255, 1.0)
|
||||
* - Or use the helper: ConvertSnesColorToImVec4() in color.cc
|
||||
*/
|
||||
class SnesColor {
|
||||
public:
|
||||
constexpr SnesColor()
|
||||
: rgb_({0.f, 0.f, 0.f, 0.f}), snes_(0), rom_color_({0, 0, 0}) {}
|
||||
: rgb_({0.f, 0.f, 0.f, 255.f}), snes_(0), rom_color_({0, 0, 0}) {}
|
||||
|
||||
/**
|
||||
* @brief Construct from ImVec4 (0.0-1.0 range)
|
||||
* @param val ImVec4 with RGB in standard 0.0-1.0 range
|
||||
*/
|
||||
explicit SnesColor(const ImVec4 val) {
|
||||
// ImVec4 from ImGui is in 0-1 range, convert to 0-255 for internal storage
|
||||
// Convert from ImGui's 0-1 range to internal 0-255 storage
|
||||
rgb_.x = val.x * kColorByteMax;
|
||||
rgb_.y = val.y * kColorByteMax;
|
||||
rgb_.z = val.z * kColorByteMax;
|
||||
@@ -55,18 +130,29 @@ class SnesColor {
|
||||
snes_ = ConvertRgbToSnes(color);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct from SNES 15-bit color
|
||||
* @param val SNES color in 15-bit BGR format
|
||||
*/
|
||||
explicit SnesColor(const uint16_t val) : snes_(val) {
|
||||
snes_color color = ConvertSnesToRgb(val);
|
||||
// ConvertSnesToRgb returns 0-255 range, store directly
|
||||
snes_color color = ConvertSnesToRgb(val); // Returns 0-255 RGB
|
||||
// Store 0-255 values in ImVec4 (unconventional but internal)
|
||||
rgb_ = ImVec4(color.red, color.green, color.blue, kColorByteMaxF);
|
||||
rom_color_ = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Construct from snes_color struct (0-255 range)
|
||||
* @param val snes_color with RGB in 0-255 range
|
||||
*/
|
||||
explicit SnesColor(const snes_color val)
|
||||
: rgb_(val.red, val.green, val.blue, kColorByteMaxF),
|
||||
snes_(ConvertRgbToSnes(val)),
|
||||
rom_color_(val) {}
|
||||
|
||||
/**
|
||||
* @brief Construct from RGB byte values (0-255)
|
||||
*/
|
||||
SnesColor(uint8_t r, uint8_t g, uint8_t b) {
|
||||
rgb_ = ImVec4(r, g, b, kColorByteMaxF);
|
||||
snes_color color;
|
||||
@@ -77,21 +163,43 @@ class SnesColor {
|
||||
rom_color_ = color;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Set color from ImVec4 (0.0-1.0 range)
|
||||
* @param val ImVec4 with RGB in standard 0.0-1.0 range
|
||||
*/
|
||||
void set_rgb(const ImVec4 val);
|
||||
|
||||
/**
|
||||
* @brief Set color from SNES 15-bit format
|
||||
* @param val SNES color in 15-bit BGR format
|
||||
*/
|
||||
void set_snes(uint16_t val);
|
||||
|
||||
/**
|
||||
* @brief Get RGB values (WARNING: stored as 0-255 in ImVec4)
|
||||
* @return ImVec4 with RGB in 0-255 range (unconventional!)
|
||||
*/
|
||||
constexpr ImVec4 rgb() const { return rgb_; }
|
||||
|
||||
/**
|
||||
* @brief Get snes_color struct (0-255 RGB)
|
||||
*/
|
||||
constexpr snes_color rom_color() const { return rom_color_; }
|
||||
|
||||
/**
|
||||
* @brief Get SNES 15-bit color
|
||||
*/
|
||||
constexpr uint16_t snes() const { return snes_; }
|
||||
|
||||
constexpr bool is_modified() const { return modified; }
|
||||
constexpr bool is_transparent() const { return transparent; }
|
||||
constexpr void set_transparent(bool t) { transparent = t; }
|
||||
constexpr void set_modified(bool m) { modified = m; }
|
||||
|
||||
private:
|
||||
ImVec4 rgb_;
|
||||
uint16_t snes_;
|
||||
snes_color rom_color_;
|
||||
ImVec4 rgb_; // Stores 0-255 values (unconventional!)
|
||||
uint16_t snes_; // 15-bit SNES format
|
||||
snes_color rom_color_; // 0-255 RGB struct
|
||||
bool modified = false;
|
||||
bool transparent = false;
|
||||
};
|
||||
|
||||
@@ -275,22 +275,53 @@ uint32_t GetPaletteAddress(const std::string &group_name, size_t palette_index,
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a palette from ROM data
|
||||
*
|
||||
* SNES ROM stores colors in 15-bit BGR format (2 bytes each):
|
||||
* - Byte 0: rrrrrggg (low byte)
|
||||
* - Byte 1: 0bbbbbgg (high byte)
|
||||
* - Full format: 0bbbbbgggggrrrrr
|
||||
*
|
||||
* This function:
|
||||
* 1. Reads SNES 15-bit colors from ROM
|
||||
* 2. Converts to RGB 0-255 range (multiply by 8 to expand 5-bit to 8-bit)
|
||||
* 3. Creates SnesColor objects that store all formats
|
||||
*
|
||||
* IMPORTANT: Transparency is NOT marked here!
|
||||
* - The SNES hardware automatically treats color index 0 of each sub-palette as transparent
|
||||
* - This is a rendering concern, not a data property
|
||||
* - ROM palette data stores actual color values, including at index 0
|
||||
* - Transparency is applied later during rendering (in SetPaletteWithTransparent or SDL)
|
||||
*
|
||||
* @param offset ROM offset to start reading
|
||||
* @param num_colors Number of colors to read
|
||||
* @param rom Pointer to ROM data
|
||||
* @return SnesPalette containing the colors (no transparency flags set)
|
||||
*/
|
||||
SnesPalette ReadPaletteFromRom(int offset, int num_colors, const uint8_t *rom) {
|
||||
int color_offset = 0;
|
||||
std::vector<gfx::SnesColor> colors(num_colors);
|
||||
|
||||
while (color_offset < num_colors) {
|
||||
short color = (uint16_t)((rom[offset + 1]) << 8) | rom[offset];
|
||||
// Read SNES 15-bit color (little endian)
|
||||
uint16_t snes_color_word = (uint16_t)((rom[offset + 1]) << 8) | rom[offset];
|
||||
|
||||
// Extract RGB components (5-bit each) and expand to 8-bit (0-255)
|
||||
snes_color new_color;
|
||||
new_color.red = (color & 0x1F) * 8;
|
||||
new_color.green = ((color >> 5) & 0x1F) * 8;
|
||||
new_color.blue = ((color >> 10) & 0x1F) * 8;
|
||||
new_color.red = (snes_color_word & 0x1F) * 8; // Bits 0-4
|
||||
new_color.green = ((snes_color_word >> 5) & 0x1F) * 8; // Bits 5-9
|
||||
new_color.blue = ((snes_color_word >> 10) & 0x1F) * 8; // Bits 10-14
|
||||
|
||||
// Create SnesColor by converting RGB back to SNES format
|
||||
// (This ensures all internal representations are consistent)
|
||||
colors[color_offset].set_snes(ConvertRgbToSnes(new_color));
|
||||
if (color_offset == 0) {
|
||||
colors[color_offset].set_transparent(true);
|
||||
}
|
||||
|
||||
// DO NOT mark as transparent - preserve actual ROM color data!
|
||||
// Transparency is handled at render time, not in the data
|
||||
|
||||
color_offset++;
|
||||
offset += 2;
|
||||
offset += 2; // SNES colors are 2 bytes each
|
||||
}
|
||||
|
||||
return gfx::SnesPalette(colors);
|
||||
@@ -318,6 +349,21 @@ absl::StatusOr<PaletteGroup> CreatePaletteGroupFromColFile(
|
||||
return palette_group;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Create a PaletteGroup by dividing a large palette into sub-palettes
|
||||
*
|
||||
* Takes a large palette (e.g., 256 colors) and divides it into smaller
|
||||
* palettes of num_colors each (typically 8 colors for SNES).
|
||||
*
|
||||
* IMPORTANT: Does NOT mark colors as transparent!
|
||||
* - Color data is preserved as-is from the source palette
|
||||
* - Transparency is a rendering concern handled by SetPaletteWithTransparent
|
||||
* - The SNES hardware handles color 0 transparency automatically
|
||||
*
|
||||
* @param palette Source palette to divide
|
||||
* @param num_colors Number of colors per sub-palette (default 8)
|
||||
* @return PaletteGroup containing the sub-palettes
|
||||
*/
|
||||
absl::StatusOr<PaletteGroup> CreatePaletteGroupFromLargePalette(
|
||||
SnesPalette &palette, int num_colors) {
|
||||
PaletteGroup palette_group;
|
||||
@@ -326,10 +372,8 @@ absl::StatusOr<PaletteGroup> CreatePaletteGroupFromLargePalette(
|
||||
if (i + num_colors <= palette.size()) {
|
||||
for (int j = 0; j < num_colors; j++) {
|
||||
auto color = palette[i + j];
|
||||
// Ensure first color of each sub-palette (index 0) is transparent
|
||||
if (j == 0) {
|
||||
color.set_transparent(true);
|
||||
}
|
||||
// DO NOT mark as transparent - preserve actual color data!
|
||||
// Transparency is handled at render time, not in the data
|
||||
new_palette.AddColor(color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,6 +351,43 @@ void CanvasContextMenu::RenderPaletteOperationsMenu(Rom* rom, gfx::Bitmap* bitma
|
||||
DisplayEditablePalette(*bitmap->mutable_palette(), "Palette", true, 8);
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Palette Help submenu
|
||||
if (ImGui::BeginMenu(ICON_MD_HELP " Palette Help")) {
|
||||
ImGui::TextColored(ImVec4(0.7F, 0.9F, 1.0F, 1.0F), "Bitmap Metadata");
|
||||
ImGui::Separator();
|
||||
|
||||
const auto& meta = bitmap->metadata();
|
||||
ImGui::Text("Source BPP: %d", meta.source_bpp);
|
||||
ImGui::Text("Palette Format: %s", meta.palette_format == 0 ? "Full" : "Sub-palette");
|
||||
ImGui::Text("Source Type: %s", meta.source_type.c_str());
|
||||
ImGui::Text("Expected Colors: %d", meta.palette_colors);
|
||||
ImGui::Text("Actual Palette Size: %zu", bitmap->palette().size());
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(1.0F, 0.9F, 0.6F, 1.0F), "Palette Application Method");
|
||||
if (meta.palette_format == 0) {
|
||||
ImGui::TextWrapped("Full palette (SetPalette) - all colors applied directly");
|
||||
} else {
|
||||
ImGui::TextWrapped("Sub-palette (SetPaletteWithTransparent) - color 0 is transparent, 1-7 from palette");
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
ImGui::TextColored(ImVec4(0.6F, 1.0F, 0.6F, 1.0F), "Documentation");
|
||||
if (ImGui::MenuItem("Palette System Architecture")) {
|
||||
ImGui::SetClipboardText("yaze/docs/palette-system-architecture.md");
|
||||
// TODO: Open file in system viewer
|
||||
}
|
||||
if (ImGui::MenuItem("User Palette Guide")) {
|
||||
ImGui::SetClipboardText("yaze/docs/user-palette-guide.md");
|
||||
// TODO: Open file in system viewer
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager) {
|
||||
}
|
||||
}
|
||||
|
||||
bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager,
|
||||
bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, CanvasPaletteManager& palette_manager,
|
||||
int group_index, int palette_index) {
|
||||
if (!bitmap) return false;
|
||||
|
||||
@@ -110,11 +110,13 @@ bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, const Canv
|
||||
bitmap->SetPaletteWithTransparent(palette, palette_index);
|
||||
}
|
||||
bitmap->set_modified(true);
|
||||
palette_manager.palette_dirty = true;
|
||||
|
||||
// Queue texture update via Arena's deferred system
|
||||
if (renderer) {
|
||||
// Queue texture update only if live_update is enabled
|
||||
if (palette_manager.live_update_enabled && renderer) {
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, bitmap);
|
||||
palette_manager.palette_dirty = false; // Clear dirty flag after update
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "app/gfx/resource/arena.h"
|
||||
#include "app/gfx/types/snes_palette.h"
|
||||
#include "app/rom.h"
|
||||
#include "imgui/imgui.h"
|
||||
@@ -62,6 +63,10 @@ struct CanvasPaletteManager {
|
||||
int current_group_index = 0;
|
||||
int current_palette_index = 0;
|
||||
|
||||
// Live update control
|
||||
bool live_update_enabled = true; // Enable/disable live texture updates
|
||||
bool palette_dirty = false; // Track if palette has changed
|
||||
|
||||
void Clear() {
|
||||
rom_palette_groups.clear();
|
||||
palette_group_names.clear();
|
||||
@@ -69,6 +74,8 @@ struct CanvasPaletteManager {
|
||||
palettes_loaded = false;
|
||||
current_group_index = 0;
|
||||
current_palette_index = 0;
|
||||
live_update_enabled = true;
|
||||
palette_dirty = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -95,9 +102,20 @@ int GetTileIdFromPosition(ImVec2 mouse_pos, float tile_size, float scale, int ti
|
||||
|
||||
// Palette management utilities
|
||||
bool LoadROMPaletteGroups(Rom* rom, CanvasPaletteManager& palette_manager);
|
||||
bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, const CanvasPaletteManager& palette_manager,
|
||||
bool ApplyPaletteGroup(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, CanvasPaletteManager& palette_manager,
|
||||
int group_index, int palette_index);
|
||||
|
||||
/**
|
||||
* @brief Apply pending palette updates (when live_update is disabled)
|
||||
*/
|
||||
inline void ApplyPendingPaletteUpdates(gfx::IRenderer* renderer, gfx::Bitmap* bitmap, CanvasPaletteManager& palette_manager) {
|
||||
if (palette_manager.palette_dirty && bitmap && renderer) {
|
||||
gfx::Arena::Get().QueueTextureCommand(
|
||||
gfx::Arena::TextureCommandType::UPDATE, bitmap);
|
||||
palette_manager.palette_dirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Drawing utility functions (moved from Canvas class)
|
||||
void DrawCanvasRect(ImDrawList* draw_list, ImVec2 canvas_p0, ImVec2 scrolling,
|
||||
int x, int y, int w, int h, ImVec4 color, float global_scale);
|
||||
|
||||
@@ -7,12 +7,30 @@
|
||||
namespace yaze {
|
||||
namespace gui {
|
||||
|
||||
/**
|
||||
* @brief Convert SnesColor to standard ImVec4 for display
|
||||
*
|
||||
* IMPORTANT: SnesColor.rgb() returns 0-255 values in ImVec4 (unconventional!)
|
||||
* This function converts them to standard ImGui format (0.0-1.0)
|
||||
*
|
||||
* @param color SnesColor with internal 0-255 storage
|
||||
* @return ImVec4 with standard 0.0-1.0 RGBA values for ImGui
|
||||
*/
|
||||
ImVec4 ConvertSnesColorToImVec4(const gfx::SnesColor& color) {
|
||||
return ImVec4(color.rgb().x / 255.0f, color.rgb().y / 255.0f,
|
||||
color.rgb().z / 255.0f, 1.0f);
|
||||
// SnesColor stores RGB as 0-255 in ImVec4, convert to standard 0-1 range
|
||||
ImVec4 rgb_255 = color.rgb();
|
||||
return ImVec4(rgb_255.x / 255.0f, rgb_255.y / 255.0f,
|
||||
rgb_255.z / 255.0f, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Convert standard ImVec4 to SnesColor
|
||||
*
|
||||
* @param color ImVec4 with standard 0.0-1.0 RGBA values
|
||||
* @return SnesColor with converted values
|
||||
*/
|
||||
gfx::SnesColor ConvertImVec4ToSnesColor(const ImVec4& color) {
|
||||
// SnesColor constructor expects 0-1 range and handles conversion internally
|
||||
return gfx::SnesColor(color);
|
||||
}
|
||||
|
||||
@@ -52,6 +70,165 @@ IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor* color,
|
||||
return changed;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// New Standardized Palette Widgets
|
||||
// ============================================================================
|
||||
|
||||
IMGUI_API bool InlinePaletteSelector(gfx::SnesPalette &palette,
|
||||
int num_colors,
|
||||
int* selected_index) {
|
||||
bool selection_made = false;
|
||||
int colors_to_show = std::min(num_colors, static_cast<int>(palette.size()));
|
||||
|
||||
ImGui::BeginGroup();
|
||||
for (int n = 0; n < colors_to_show; n++) {
|
||||
ImGui::PushID(n);
|
||||
if (n > 0 && (n % 8) != 0) {
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y);
|
||||
}
|
||||
|
||||
bool is_selected = selected_index && (*selected_index == n);
|
||||
if (is_selected) {
|
||||
ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 1.0f, 0.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 2.0f);
|
||||
}
|
||||
|
||||
if (SnesColorButton("##palettesel", palette[n],
|
||||
ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker,
|
||||
ImVec2(20, 20))) {
|
||||
if (selected_index) {
|
||||
*selected_index = n;
|
||||
selection_made = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_selected) {
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
return selection_made;
|
||||
}
|
||||
|
||||
IMGUI_API absl::Status InlinePaletteEditor(gfx::SnesPalette &palette,
|
||||
const std::string &title,
|
||||
ImGuiColorEditFlags flags) {
|
||||
if (!title.empty()) {
|
||||
ImGui::Text("%s", title.c_str());
|
||||
}
|
||||
|
||||
static int selected_color = 0;
|
||||
static ImVec4 current_color = ImVec4(0, 0, 0, 1.0f);
|
||||
|
||||
// Color picker
|
||||
ImGui::Separator();
|
||||
if (ImGui::ColorPicker4("##colorpicker", (float*)¤t_color,
|
||||
ImGuiColorEditFlags_NoSidePreview |
|
||||
ImGuiColorEditFlags_NoSmallPreview)) {
|
||||
gfx::SnesColor snes_color(current_color);
|
||||
palette.UpdateColor(selected_color, snes_color);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Palette grid
|
||||
ImGui::BeginGroup();
|
||||
for (int n = 0; n < palette.size(); n++) {
|
||||
ImGui::PushID(n);
|
||||
if ((n % 8) != 0) {
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y);
|
||||
}
|
||||
|
||||
if (flags == 0) {
|
||||
flags = ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker;
|
||||
}
|
||||
|
||||
if (SnesColorButton("##palettedit", palette[n], flags, ImVec2(20, 20))) {
|
||||
selected_color = n;
|
||||
current_color = ConvertSnesColorToImVec4(palette[n]);
|
||||
}
|
||||
|
||||
// Context menu
|
||||
if (ImGui::BeginPopupContextItem()) {
|
||||
if (ImGui::MenuItem("Copy as SNES")) {
|
||||
std::string clipboard = absl::StrFormat("$%04X", palette[n].snes());
|
||||
ImGui::SetClipboardText(clipboard.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem("Copy as RGB")) {
|
||||
auto rgb = palette[n].rgb();
|
||||
std::string clipboard = absl::StrFormat("(%d,%d,%d)",
|
||||
(int)rgb.x, (int)rgb.y, (int)rgb.z);
|
||||
ImGui::SetClipboardText(clipboard.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem("Copy as Hex")) {
|
||||
auto rgb = palette[n].rgb();
|
||||
std::string clipboard = absl::StrFormat("#%02X%02X%02X",
|
||||
(int)rgb.x, (int)rgb.y, (int)rgb.z);
|
||||
ImGui::SetClipboardText(clipboard.c_str());
|
||||
}
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
return absl::OkStatus();
|
||||
}
|
||||
|
||||
IMGUI_API bool PopupPaletteEditor(const char* popup_id,
|
||||
gfx::SnesPalette &palette,
|
||||
ImGuiColorEditFlags flags) {
|
||||
bool modified = false;
|
||||
|
||||
if (ImGui::BeginPopup(popup_id)) {
|
||||
static int selected_color = 0;
|
||||
static ImVec4 current_color = ImVec4(0, 0, 0, 1.0f);
|
||||
|
||||
// Compact color picker
|
||||
if (ImGui::ColorPicker4("##popuppicker", (float*)¤t_color,
|
||||
ImGuiColorEditFlags_NoSidePreview |
|
||||
ImGuiColorEditFlags_NoSmallPreview)) {
|
||||
gfx::SnesColor snes_color(current_color);
|
||||
palette.UpdateColor(selected_color, snes_color);
|
||||
modified = true;
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
// Palette grid
|
||||
ImGui::BeginGroup();
|
||||
for (int n = 0; n < palette.size() && n < 64; n++) { // Limit to 64 for popup
|
||||
ImGui::PushID(n);
|
||||
if ((n % 8) != 0) {
|
||||
ImGui::SameLine(0.0f, ImGui::GetStyle().ItemSpacing.y);
|
||||
}
|
||||
|
||||
if (SnesColorButton("##popuppal", palette[n],
|
||||
ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoPicker,
|
||||
ImVec2(20, 20))) {
|
||||
selected_color = n;
|
||||
current_color = ConvertSnesColorToImVec4(palette[n]);
|
||||
}
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::EndGroup();
|
||||
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Legacy Functions (for compatibility)
|
||||
// ============================================================================
|
||||
|
||||
IMGUI_API bool DisplayPalette(gfx::SnesPalette& palette, bool loaded) {
|
||||
static ImVec4 color = ImVec4(0, 0, 0, 255.f);
|
||||
ImGuiColorEditFlags misc_flags = ImGuiColorEditFlags_AlphaPreview |
|
||||
|
||||
@@ -45,6 +45,44 @@ IMGUI_API bool SnesColorButton(absl::string_view id, gfx::SnesColor &color,
|
||||
IMGUI_API bool SnesColorEdit4(absl::string_view label, gfx::SnesColor *color,
|
||||
ImGuiColorEditFlags flags = 0);
|
||||
|
||||
// ============================================================================
|
||||
// Palette Widget Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief Small inline palette selector - just color buttons for selection
|
||||
* @param palette Palette to display
|
||||
* @param num_colors Number of colors to show (default 8)
|
||||
* @param selected_index Pointer to store selected color index (optional)
|
||||
* @return True if a color was selected
|
||||
*/
|
||||
IMGUI_API bool InlinePaletteSelector(gfx::SnesPalette &palette,
|
||||
int num_colors = 8,
|
||||
int* selected_index = nullptr);
|
||||
|
||||
/**
|
||||
* @brief Full inline palette editor with color picker and copy options
|
||||
* @param palette Palette to edit
|
||||
* @param title Display title
|
||||
* @param flags ImGui color edit flags
|
||||
* @return Status of the operation
|
||||
*/
|
||||
IMGUI_API absl::Status InlinePaletteEditor(gfx::SnesPalette &palette,
|
||||
const std::string &title = "",
|
||||
ImGuiColorEditFlags flags = 0);
|
||||
|
||||
/**
|
||||
* @brief Popup palette editor - same as inline but in a popup
|
||||
* @param popup_id ID for the popup window
|
||||
* @param palette Palette to edit
|
||||
* @param flags ImGui color edit flags
|
||||
* @return True if palette was modified
|
||||
*/
|
||||
IMGUI_API bool PopupPaletteEditor(const char* popup_id,
|
||||
gfx::SnesPalette &palette,
|
||||
ImGuiColorEditFlags flags = 0);
|
||||
|
||||
// Legacy functions (kept for compatibility, will be deprecated)
|
||||
IMGUI_API bool DisplayPalette(gfx::SnesPalette &palette, bool loaded);
|
||||
|
||||
IMGUI_API absl::Status DisplayEditablePalette(gfx::SnesPalette &palette,
|
||||
|
||||
Reference in New Issue
Block a user