From 668fdc806866efba145db189524b7f41f4d7a394 Mon Sep 17 00:00:00 2001 From: scawful Date: Mon, 13 Oct 2025 17:06:10 -0400 Subject: [PATCH] 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. --- src/app/gfx/core/bitmap.cc | 138 +++++++++++++---- src/app/gfx/core/bitmap.h | 26 ++++ src/app/gfx/types/snes_color.cc | 18 ++- src/app/gfx/types/snes_color.h | 132 ++++++++++++++-- src/app/gfx/types/snes_palette.cc | 68 ++++++-- src/app/gui/canvas/canvas_context_menu.cc | 37 +++++ src/app/gui/canvas/canvas_utils.cc | 8 +- src/app/gui/canvas/canvas_utils.h | 20 ++- src/app/gui/core/color.cc | 181 +++++++++++++++++++++- src/app/gui/core/color.h | 38 +++++ 10 files changed, 600 insertions(+), 66 deletions(-) diff --git a/src/app/gfx/core/bitmap.cc b/src/app/gfx/core/bitmap.cc index 56bf2e5f..31989a5b 100644 --- a/src/app/gfx/core/bitmap.cc +++ b/src/app/gfx/core/bitmap.cc @@ -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(pal_color.rgb().x); - sdl_palette->colors[i].g = static_cast(pal_color.rgb().y); - sdl_palette->colors[i].b = static_cast(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(rgb_255.x); + sdl_palette->colors[i].g = static_cast(rgb_255.y); + sdl_palette->colors[i].b = static_cast(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 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(colors.size()); ++color_index) { if (color_index < surface_->format->palette->ncolors) { - surface_->format->palette->colors[color_index].r = static_cast(colors[color_index].x * 255); - surface_->format->palette->colors[color_index].g = static_cast(colors[color_index].y * 255); - surface_->format->palette->colors[color_index].b = static_cast(colors[color_index].z * 255); - surface_->format->palette->colors[color_index].a = static_cast(colors[color_index].w * 255); + surface_->format->palette->colors[color_index].r = static_cast(colors[color_index].x * 255.0f); + surface_->format->palette->colors[color_index].g = static_cast(colors[color_index].y * 255.0f); + surface_->format->palette->colors[color_index].b = static_cast(colors[color_index].z * 255.0f); + surface_->format->palette->colors[color_index].a = static_cast(colors[color_index].w * 255.0f); } } SDL_LockSurface(surface_); diff --git a/src/app/gfx/core/bitmap.h b/src/app/gfx/core/bitmap.h index d7a00e39..14837f61 100644 --- a/src/app/gfx/core/bitmap.h +++ b/src/app/gfx/core/bitmap.h @@ -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 &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 data_; diff --git a/src/app/gfx/types/snes_color.cc b/src/app/gfx/types/snes_color.cc index cdb5ff8e..73fc151d 100644 --- a/src/app/gfx/types/snes_color.cc +++ b/src/app/gfx/types/snes_color.cc @@ -99,14 +99,14 @@ std::vector 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(rgb_.x); color.green = static_cast(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(col.red), + static_cast(col.green), + static_cast(col.blue), + kColorByteMaxF); + rom_color_ = col; modified = true; } diff --git a/src/app/gfx/types/snes_color.h b/src/app/gfx/types/snes_color.h index bed3737f..6cd0f6ee 100644 --- a/src/app/gfx/types/snes_color.h +++ b/src/app/gfx/types/snes_color.h @@ -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(color.x * 255.0f); + result.green = static_cast(color.y * 255.0f); + result.blue = static_cast(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 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; }; diff --git a/src/app/gfx/types/snes_palette.cc b/src/app/gfx/types/snes_palette.cc index a577f499..c5de4cc3 100644 --- a/src/app/gfx/types/snes_palette.cc +++ b/src/app/gfx/types/snes_palette.cc @@ -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 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 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 CreatePaletteGroupFromLargePalette( SnesPalette &palette, int num_colors) { PaletteGroup palette_group; @@ -326,10 +372,8 @@ absl::StatusOr 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); } } diff --git a/src/app/gui/canvas/canvas_context_menu.cc b/src/app/gui/canvas/canvas_context_menu.cc index d907dac6..b2efd914 100644 --- a/src/app/gui/canvas/canvas_context_menu.cc +++ b/src/app/gui/canvas/canvas_context_menu.cc @@ -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(); } } diff --git a/src/app/gui/canvas/canvas_utils.cc b/src/app/gui/canvas/canvas_utils.cc index 9459dca8..716bc592 100644 --- a/src/app/gui/canvas/canvas_utils.cc +++ b/src/app/gui/canvas/canvas_utils.cc @@ -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; } diff --git a/src/app/gui/canvas/canvas_utils.h b/src/app/gui/canvas/canvas_utils.h index 73aa8e35..e118bc72 100644 --- a/src/app/gui/canvas/canvas_utils.h +++ b/src/app/gui/canvas/canvas_utils.h @@ -3,6 +3,7 @@ #include #include +#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); diff --git a/src/app/gui/core/color.cc b/src/app/gui/core/color.cc index c8ed62ec..e80b2f23 100644 --- a/src/app/gui/core/color.cc +++ b/src/app/gui/core/color.cc @@ -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(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 | diff --git a/src/app/gui/core/color.h b/src/app/gui/core/color.h index bc542ba7..62cbfc00 100644 --- a/src/app/gui/core/color.h +++ b/src/app/gui/core/color.h @@ -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,