2903 lines
		
	
	
		
			122 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			2903 lines
		
	
	
		
			122 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
// Dear ImGui: standalone example application for SDL2 + OpenGL
 | 
						|
// (SDL is a cross-platform general purpose library for handling windows,
 | 
						|
// inputs, OpenGL/Vulkan/Metal graphics context creation, etc.)
 | 
						|
 | 
						|
// Learn about Dear ImGui:
 | 
						|
// - FAQ                  https://dearimgui.com/faq
 | 
						|
// - Getting Started      https://dearimgui.com/getting-started
 | 
						|
// - Documentation        https://dearimgui.com/docs (same as your local docs/
 | 
						|
// folder).
 | 
						|
// - Introduction, links and more at the top of imgui.cpp
 | 
						|
 | 
						|
#define IMGUI_DEFINE_MATH_OPERATORS
 | 
						|
#include "imgui.h"
 | 
						|
#include "imgui_impl_opengl3.h"
 | 
						|
#include "imgui_impl_sdl2.h"
 | 
						|
#include <GL/glew.h>
 | 
						|
#include <SDL.h>
 | 
						|
#include <stdio.h>
 | 
						|
#if defined(IMGUI_IMPL_OPENGL_ES2)
 | 
						|
#include <SDL_opengles2.h>
 | 
						|
#else
 | 
						|
#include <GL/gl.h>
 | 
						|
#include <SDL_opengl.h>
 | 
						|
#endif
 | 
						|
 | 
						|
// This example can also compile and run with Emscripten! See
 | 
						|
// 'Makefile.emscripten' for details.
 | 
						|
#ifdef __EMSCRIPTEN__
 | 
						|
#include "../libs/emscripten/emscripten_mainloop_stub.h"
 | 
						|
#endif
 | 
						|
 | 
						|
#include "exif.h"
 | 
						|
 | 
						|
#define APP_IMAGE_IMPLEMENTATION
 | 
						|
#define IMGUI_IMAGE_VIEWER_IMPLEMENTATION
 | 
						|
 | 
						|
#include "app_image.h"
 | 
						|
#include "imgui_tex_inspect.h"
 | 
						|
#include "shaderutils.h"
 | 
						|
#include "tex_inspect_opengl.h"
 | 
						|
 | 
						|
static float exposure = 0.0f;
 | 
						|
static float contrast = 0.0f;
 | 
						|
static float highlights = 0.0f;
 | 
						|
static float shadows = 0.0f;
 | 
						|
static float whites = 0.0f;
 | 
						|
static float blacks = 0.0f;
 | 
						|
static float temperature = 6500.0f; // Example starting point (Kelvin)
 | 
						|
static float tint = 0.0f;
 | 
						|
static float vibrance = 0.0f;
 | 
						|
static float saturation = 0.0f;
 | 
						|
static float clarity = 0.0f;
 | 
						|
static float texture = 0.0f;
 | 
						|
static float dehaze = 0.0f;
 | 
						|
 | 
						|
#include <functional> // For std::function
 | 
						|
#include <map>
 | 
						|
#include <memory> // For unique_ptr
 | 
						|
#include <string>
 | 
						|
#include <vector>
 | 
						|
 | 
						|
#include "imfilebrowser.h" // <<< Add this
 | 
						|
#include <filesystem>      // <<< Add for path manipulation (C++17)
 | 
						|
 | 
						|
struct ShaderUniform
 | 
						|
{
 | 
						|
    std::string name;
 | 
						|
    GLint location = -1;
 | 
						|
    // Add type info if needed for different glUniform calls, or handle in setter
 | 
						|
};
 | 
						|
 | 
						|
struct PipelineOperation
 | 
						|
{
 | 
						|
    std::string name;
 | 
						|
    GLuint shaderProgram = 0;
 | 
						|
    bool enabled = true;
 | 
						|
    std::map<std::string, ShaderUniform> uniforms; // Map uniform name to its info
 | 
						|
 | 
						|
    // Function to update uniforms based on global slider values etc.
 | 
						|
    std::function<void(GLuint /*program*/)> updateUniformsCallback;
 | 
						|
 | 
						|
    // Store the actual slider variable pointers for direct modification in ImGui
 | 
						|
    // This avoids needing complex callbacks for simple sliders
 | 
						|
    float *exposureVal = nullptr;
 | 
						|
    float *contrastVal = nullptr;
 | 
						|
    float *highlightsVal = nullptr;
 | 
						|
    float *shadowsVal = nullptr;
 | 
						|
    float *whitesVal = nullptr;
 | 
						|
    float *blacksVal = nullptr;
 | 
						|
    float *temperatureVal = nullptr;
 | 
						|
    float *tintVal = nullptr;
 | 
						|
    float *vibranceVal = nullptr;
 | 
						|
    float *saturationVal = nullptr;
 | 
						|
    float *clarityVal = nullptr;
 | 
						|
    float *textureVal = nullptr;
 | 
						|
    float *dehazeVal = nullptr;
 | 
						|
    // ... add pointers for other controls as needed
 | 
						|
 | 
						|
    PipelineOperation(std::string n) : name(std::move(n)) {}
 | 
						|
 | 
						|
    void FindUniformLocations()
 | 
						|
    {
 | 
						|
        if (!shaderProgram)
 | 
						|
            return;
 | 
						|
        for (auto &pair : uniforms)
 | 
						|
        {
 | 
						|
            pair.second.location =
 | 
						|
                glGetUniformLocation(shaderProgram, pair.second.name.c_str());
 | 
						|
            if (pair.second.location == -1 && name != "Passthrough" &&
 | 
						|
                name != "LinearToSRGB" &&
 | 
						|
                name != "SRGBToLinear")
 | 
						|
            { // Ignore for simple shaders
 | 
						|
                // Don't treat missing texture samplers as errors here, they are set
 | 
						|
                // explicitly
 | 
						|
                if (pair.second.name != "InputTexture")
 | 
						|
                {
 | 
						|
                    fprintf(stderr, "Warning: Uniform '%s' not found in shader '%s'\n",
 | 
						|
                            pair.second.name.c_str(), name.c_str());
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
// Enum for Color Spaces (expand later)
 | 
						|
enum class ColorSpace
 | 
						|
{
 | 
						|
    LINEAR_SRGB, // Linear Rec.709/sRGB primaries
 | 
						|
    SRGB         // Non-linear sRGB (display)
 | 
						|
                 // Add AdobeRGB, ProPhoto etc. later
 | 
						|
};
 | 
						|
 | 
						|
const char *ColorSpaceToString(ColorSpace cs)
 | 
						|
{
 | 
						|
    switch (cs)
 | 
						|
    {
 | 
						|
    case ColorSpace::LINEAR_SRGB:
 | 
						|
        return "Linear sRGB";
 | 
						|
    case ColorSpace::SRGB:
 | 
						|
        return "sRGB";
 | 
						|
    default:
 | 
						|
        return "Unknown";
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
bool ReadTextureToAppImage(GLuint textureId, int width, int height,
 | 
						|
                           AppImage &outImage)
 | 
						|
{
 | 
						|
    if (textureId == 0 || width <= 0 || height <= 0)
 | 
						|
    {
 | 
						|
        fprintf(stderr, "ReadTextureToAppImage: Invalid parameters.\n");
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // We assume the texture 'textureId' holds LINEAR RGBA FLOAT data (e.g.,
 | 
						|
    // GL_RGBA16F) Resize AppImage to hold the data
 | 
						|
    outImage.resize(width, height,
 | 
						|
                    4);         // Expecting 4 channels (RGBA) from pipeline texture
 | 
						|
    outImage.m_isLinear = true; // Data we read back should be linear
 | 
						|
    outImage.m_colorSpaceName =
 | 
						|
        "Linear sRGB"; // Assuming pipeline used sRGB primaries
 | 
						|
 | 
						|
    std::vector<float> &pixelData = outImage.getPixelVector();
 | 
						|
    if (pixelData.empty())
 | 
						|
    {
 | 
						|
        fprintf(stderr,
 | 
						|
                "ReadTextureToAppImage: Failed to allocate AppImage buffer.\n");
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Bind the texture
 | 
						|
    GLint lastTexture;
 | 
						|
    glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
 | 
						|
    glBindTexture(GL_TEXTURE_2D, textureId);
 | 
						|
 | 
						|
    // Set alignment (good practice)
 | 
						|
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
 | 
						|
 | 
						|
    // Read the pixels
 | 
						|
    // We request GL_RGBA and GL_FLOAT as that's our assumed linear working format
 | 
						|
    // on GPU
 | 
						|
    glGetTexImage(GL_TEXTURE_2D,
 | 
						|
                  0,                 // Mipmap level 0
 | 
						|
                  GL_RGBA,           // Request RGBA format
 | 
						|
                  GL_FLOAT,          // Request float data type
 | 
						|
                  pixelData.data()); // Pointer to destination buffer
 | 
						|
 | 
						|
    GLenum err = glGetError();
 | 
						|
    glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding
 | 
						|
 | 
						|
    if (err != GL_NO_ERROR)
 | 
						|
    {
 | 
						|
        fprintf(stderr,
 | 
						|
                "ReadTextureToAppImage: OpenGL Error during glGetTexImage: %u\n",
 | 
						|
                err);
 | 
						|
        outImage.clear_image(); // Clear invalid data
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    printf("ReadTextureToAppImage: Successfully read %dx%d texture.\n", width,
 | 
						|
           height);
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
class ImageProcessingPipeline
 | 
						|
{
 | 
						|
private:
 | 
						|
    GLuint m_fbo[2] = {0, 0};
 | 
						|
    GLuint m_tex[2] = {0, 0}; // Ping-pong textures
 | 
						|
    GLuint m_vao = 0;
 | 
						|
    GLuint m_vbo = 0;
 | 
						|
    int m_texWidth = 0;
 | 
						|
    int m_texHeight = 0;
 | 
						|
    GLuint m_passthroughShader = 0;
 | 
						|
    GLuint m_linearToSrgbShader = 0;
 | 
						|
    GLuint m_srgbToLinearShader = 0;
 | 
						|
    GLuint m_diffShader = 0; // <<< New shader for diffing
 | 
						|
 | 
						|
    // --- Caching Members ---
 | 
						|
    bool m_isDirty = true;               // Flag to indicate if reprocessing is needed
 | 
						|
    GLuint m_cachedDisplayTextureId = 0; // Cached ID for display (potentially sRGB)
 | 
						|
    GLuint m_cachedLinearTextureId = 0;  // Cached ID for saving (always linear)
 | 
						|
    int m_cachedInputWidth = 0;          // Track dimensions for cache validity
 | 
						|
    int m_cachedInputHeight = 0;
 | 
						|
    GLuint m_lastProcessedInputTextureId = 0; // Track input texture for cache validity
 | 
						|
 | 
						|
    // --- Diffing State ---
 | 
						|
    bool m_diffActive = false;
 | 
						|
    std::string m_diffActiveOperationName = ""; // Name of the operation being diffed
 | 
						|
    GLuint m_diffTexBefore = 0;                 // Texture ID *before* the diffed operation
 | 
						|
    GLuint m_diffTexAfter = 0;                  // Texture ID *after* the diffed operation (in the current step)
 | 
						|
 | 
						|
    // Helper to find operation index by name
 | 
						|
    std::optional<int> FindOperationIndex(const std::string &name) const
 | 
						|
    {
 | 
						|
        for (size_t i = 0; i < activeOperations.size(); ++i)
 | 
						|
        {
 | 
						|
            if (activeOperations[i].name == name)
 | 
						|
            {
 | 
						|
                return static_cast<int>(i);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        return std::nullopt;
 | 
						|
    }
 | 
						|
 | 
						|
    void CreateFullscreenQuad()
 | 
						|
    {
 | 
						|
        // Simple quad covering -1 to 1 in x,y and 0 to 1 in u,v
 | 
						|
        float vertices[] = {// positions // texCoords
 | 
						|
                            -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, -1.0f,
 | 
						|
                            0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f,
 | 
						|
 | 
						|
                            -1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f,
 | 
						|
                            1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
 | 
						|
        printf("Matrix ready.\n");
 | 
						|
 | 
						|
        glGenVertexArrays(1, &m_vao);
 | 
						|
        printf("Fullscreen quad VAO created.\n");
 | 
						|
        glGenBuffers(1, &m_vbo);
 | 
						|
        printf("Fullscreen quad VBO created.\n");
 | 
						|
 | 
						|
        glBindVertexArray(m_vao);
 | 
						|
        glBindBuffer(GL_ARRAY_BUFFER, m_vbo);
 | 
						|
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 | 
						|
        printf("Fullscreen quad VBO created.\n");
 | 
						|
 | 
						|
        // Position attribute
 | 
						|
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
 | 
						|
                              (void *)0);
 | 
						|
        glEnableVertexAttribArray(0);
 | 
						|
        // Texture coordinate attribute
 | 
						|
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
 | 
						|
                              (void *)(2 * sizeof(float)));
 | 
						|
        glEnableVertexAttribArray(1);
 | 
						|
 | 
						|
        glBindBuffer(GL_ARRAY_BUFFER, 0);
 | 
						|
        glBindVertexArray(0);
 | 
						|
        printf("Fullscreen quad VAO/VBO created.\n");
 | 
						|
    }
 | 
						|
 | 
						|
    bool CreateOrResizeFBOs(int width, int height)
 | 
						|
    {
 | 
						|
        if (width == m_texWidth && height == m_texHeight && m_fbo[0] != 0)
 | 
						|
        {
 | 
						|
            return true; // Already correct size
 | 
						|
        }
 | 
						|
 | 
						|
        if (width <= 0 || height <= 0)
 | 
						|
        {
 | 
						|
            fprintf(stderr, "CreateOrResizeFBOs: Invalid dimensions (%dx%d).\n", width, height);
 | 
						|
            return false; // Invalid dimensions
 | 
						|
        }
 | 
						|
 | 
						|
        // Cleanup existing before creating new ones
 | 
						|
        DestroyFBOs(); // This also invalidates cached texture IDs internally
 | 
						|
 | 
						|
        m_texWidth = width;
 | 
						|
        m_texHeight = height;
 | 
						|
        printf("Pipeline: Creating/Resizing FBOs and Textures to %dx%d.\n", width, height);
 | 
						|
 | 
						|
        glGenFramebuffers(2, m_fbo);
 | 
						|
        glGenTextures(2, m_tex);
 | 
						|
 | 
						|
        GLint lastTexture;
 | 
						|
        glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture);
 | 
						|
        GLint lastFBO;
 | 
						|
        glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO);
 | 
						|
 | 
						|
        bool success = true;
 | 
						|
        for (int i = 0; i < 2; ++i)
 | 
						|
        {
 | 
						|
            glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[i]);
 | 
						|
            glBindTexture(GL_TEXTURE_2D, m_tex[i]);
 | 
						|
 | 
						|
            // Using RGBA16F for high precision pipeline
 | 
						|
            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr);
 | 
						|
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // Linear filtering is often better for display
 | 
						|
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
 | 
						|
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 | 
						|
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 | 
						|
 | 
						|
            glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex[i], 0);
 | 
						|
 | 
						|
            GLenum fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER);
 | 
						|
            if (fboStatus != GL_FRAMEBUFFER_COMPLETE)
 | 
						|
            {
 | 
						|
                fprintf(stderr, "ERROR::FRAMEBUFFER:: Framebuffer %d is not complete! Status: 0x%x\n", i, fboStatus);
 | 
						|
                success = false;
 | 
						|
                break; // Stop if one fails
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                printf("FBO %d (Texture %d) created successfully (%dx%d).\n", m_fbo[i], m_tex[i], width, height);
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        glBindTexture(GL_TEXTURE_2D, lastTexture);
 | 
						|
        glBindFramebuffer(GL_FRAMEBUFFER, lastFBO);
 | 
						|
 | 
						|
        if (!success)
 | 
						|
        {
 | 
						|
            fprintf(stderr, "Pipeline: FBO creation failed. Cleaning up.\n");
 | 
						|
            DestroyFBOs(); // Clean up partial setup
 | 
						|
            return false;
 | 
						|
        }
 | 
						|
 | 
						|
        // Successfully created, mark dirty because resources changed
 | 
						|
        MarkDirty();
 | 
						|
        return true;
 | 
						|
    }
 | 
						|
 | 
						|
    void DestroyFBOs()
 | 
						|
    {
 | 
						|
        if (m_fbo[0])
 | 
						|
        {
 | 
						|
            glDeleteFramebuffers(2, m_fbo);
 | 
						|
            m_fbo[0] = m_fbo[1] = 0;
 | 
						|
        }
 | 
						|
        if (m_tex[0])
 | 
						|
        {
 | 
						|
            glDeleteTextures(2, m_tex);
 | 
						|
            m_tex[0] = m_tex[1] = 0;
 | 
						|
        }
 | 
						|
        // Invalidate cache when FBOs are destroyed
 | 
						|
        m_texWidth = m_texHeight = 0;
 | 
						|
        m_cachedDisplayTextureId = 0;
 | 
						|
        m_cachedLinearTextureId = 0;
 | 
						|
        m_cachedInputWidth = 0;
 | 
						|
        m_cachedInputHeight = 0;
 | 
						|
        m_lastProcessedInputTextureId = 0;
 | 
						|
        m_isDirty = true; // Need reprocessing after destruction
 | 
						|
        printf("Pipeline: Destroyed FBOs and textures. Cache invalidated.\n");
 | 
						|
    }
 | 
						|
 | 
						|
    struct ProcessingResult
 | 
						|
    {
 | 
						|
        GLuint linearOutput = 0;
 | 
						|
        GLuint displayOutput = 0;
 | 
						|
        GLuint diffOutput = 0; // Only valid if diff was active during processing
 | 
						|
    };
 | 
						|
 | 
						|
    ProcessingResult ExecuteProcessingSteps(GLuint inputTextureId, int width, int height)
 | 
						|
    {
 | 
						|
        ProcessingResult result = {};
 | 
						|
        m_diffTexBefore = 0; // Reset diff textures for this run
 | 
						|
        m_diffTexAfter = 0;
 | 
						|
 | 
						|
        if (inputTextureId == 0 || width <= 0 || height <= 0 || !m_diffShader)
 | 
						|
        {
 | 
						|
            fprintf(stderr, "ExecuteProcessingSteps: Invalid input, state, or missing diff shader.\n");
 | 
						|
            return result; // Return empty result
 | 
						|
        }
 | 
						|
        printf("Pipeline: Executing processing steps for %dx%d image (Input TexID: %u). Diff Active: %s (%s)\n",
 | 
						|
               width, height, inputTextureId, m_diffActive ? "Yes" : "No", m_diffActiveOperationName.c_str());
 | 
						|
 | 
						|
        if (!CreateOrResizeFBOs(width, height))
 | 
						|
        {
 | 
						|
            fprintf(stderr, "ExecuteProcessingSteps: Failed to create/resize FBOs.\n");
 | 
						|
            return result;
 | 
						|
        }
 | 
						|
 | 
						|
        // Store original state
 | 
						|
        GLint viewport[4];
 | 
						|
        glGetIntegerv(GL_VIEWPORT, viewport);
 | 
						|
        GLint lastFBO;
 | 
						|
        glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO);
 | 
						|
        // ... (save other relevant state: active texture, program, vao etc) ...
 | 
						|
        GLint lastActiveTexture;
 | 
						|
        glGetIntegerv(GL_ACTIVE_TEXTURE, &lastActiveTexture);
 | 
						|
        GLint lastBoundTexture;
 | 
						|
        glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastBoundTexture);
 | 
						|
        GLint lastProgram;
 | 
						|
        glGetIntegerv(GL_CURRENT_PROGRAM, &lastProgram);
 | 
						|
        GLint lastVao;
 | 
						|
        glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &lastVao);
 | 
						|
 | 
						|
        glViewport(0, 0, m_texWidth, m_texHeight);
 | 
						|
        glBindVertexArray(m_vao);
 | 
						|
 | 
						|
        int currentBufferIndex = 0;               // Index into m_fbo/m_tex (0 or 1) -> Target for WRITING
 | 
						|
        GLuint currentReadTexId = inputTextureId; // Texture to READ from
 | 
						|
 | 
						|
        std::optional<int> diffOpIndexOpt = std::nullopt;
 | 
						|
        if (m_diffActive && !m_diffActiveOperationName.empty())
 | 
						|
        {
 | 
						|
            diffOpIndexOpt = FindOperationIndex(m_diffActiveOperationName);
 | 
						|
            if (!diffOpIndexOpt)
 | 
						|
            {
 | 
						|
                fprintf(stderr, "Warning: Diff active but couldn't find operation '%s'\n", m_diffActiveOperationName.c_str());
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // --- Input Color Space Conversion (if needed) ---
 | 
						|
        bool handledInitialStep = false;
 | 
						|
        // ... (Input conversion logic remains the same as before) ...
 | 
						|
        if (inputColorSpace == ColorSpace::SRGB)
 | 
						|
        {
 | 
						|
            glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentBufferIndex]);
 | 
						|
            glUseProgram(m_srgbToLinearShader);
 | 
						|
            glActiveTexture(GL_TEXTURE0);
 | 
						|
            glBindTexture(GL_TEXTURE_2D, currentReadTexId);
 | 
						|
            glUniform1i(glGetUniformLocation(m_srgbToLinearShader, "InputTexture"), 0);
 | 
						|
            glDrawArrays(GL_TRIANGLES, 0, 6);
 | 
						|
            currentReadTexId = m_tex[currentBufferIndex];
 | 
						|
            currentBufferIndex = 1 - currentBufferIndex;
 | 
						|
            handledInitialStep = true;
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            bool anyUserOpsEnabled = false; /* check if any op is enabled */
 | 
						|
            for (const auto &op : activeOperations)
 | 
						|
                if (op.enabled && op.shaderProgram)
 | 
						|
                    anyUserOpsEnabled = true;
 | 
						|
 | 
						|
            if (anyUserOpsEnabled)
 | 
						|
            {
 | 
						|
                glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentBufferIndex]);
 | 
						|
                glUseProgram(m_passthroughShader);
 | 
						|
                glActiveTexture(GL_TEXTURE0);
 | 
						|
                glBindTexture(GL_TEXTURE_2D, currentReadTexId);
 | 
						|
                glUniform1i(glGetUniformLocation(m_passthroughShader, "InputTexture"), 0);
 | 
						|
                glDrawArrays(GL_TRIANGLES, 0, 6);
 | 
						|
                currentReadTexId = m_tex[currentBufferIndex];
 | 
						|
                currentBufferIndex = 1 - currentBufferIndex;
 | 
						|
                handledInitialStep = true;
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                handledInitialStep = false;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // --- Apply Editing Operations ---
 | 
						|
        int appliedOpsCount = 0;
 | 
						|
        for (size_t i = 0; i < activeOperations.size(); ++i)
 | 
						|
        {
 | 
						|
            const auto &op = activeOperations[i];
 | 
						|
            if (op.enabled && op.shaderProgram)
 | 
						|
            {
 | 
						|
                // <<< DIFF CAPTURE: Before processing the target operation >>>
 | 
						|
                if (diffOpIndexOpt && (int)i == *diffOpIndexOpt)
 | 
						|
                {
 | 
						|
                    m_diffTexBefore = currentReadTexId; // Capture the input to this step
 | 
						|
                    printf("Pipeline: Capturing TexID %u as 'Before' for diff op '%s'\n", m_diffTexBefore, op.name.c_str());
 | 
						|
                }
 | 
						|
 | 
						|
                glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentBufferIndex]);
 | 
						|
                glUseProgram(op.shaderProgram);
 | 
						|
                glActiveTexture(GL_TEXTURE0);
 | 
						|
                glBindTexture(GL_TEXTURE_2D, currentReadTexId);
 | 
						|
                GLint loc = glGetUniformLocation(op.shaderProgram, "InputTexture");
 | 
						|
                if (loc != -1)
 | 
						|
                    glUniform1i(loc, 0);
 | 
						|
 | 
						|
                // Set uniforms (as before)
 | 
						|
                if (op.updateUniformsCallback)
 | 
						|
                {
 | 
						|
                    op.updateUniformsCallback(op.shaderProgram);
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    // (Your existing uniform setting logic based on op.*Val pointers)
 | 
						|
                    // Example:
 | 
						|
                    if (op.exposureVal && op.uniforms.count("exposureValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("exposureValue").location, *op.exposureVal);
 | 
						|
                    if (op.contrastVal && op.uniforms.count("contrastValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("contrastValue").location, *op.contrastVal);
 | 
						|
                    if (op.highlightsVal && op.uniforms.count("highlightsValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("highlightsValue").location, *op.highlightsVal);
 | 
						|
                    if (op.shadowsVal && op.uniforms.count("shadowsValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("shadowsValue").location, *op.shadowsVal);
 | 
						|
                    if (op.whitesVal && op.uniforms.count("whitesValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("whitesValue").location, *op.whitesVal);
 | 
						|
                    if (op.blacksVal && op.uniforms.count("blacksValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("blacksValue").location, *op.blacksVal);
 | 
						|
                    if (op.temperatureVal && op.uniforms.count("temperatureValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("temperatureValue").location, *op.temperatureVal);
 | 
						|
                    if (op.tintVal && op.uniforms.count("tintValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("tintValue").location, *op.tintVal);
 | 
						|
                    if (op.vibranceVal && op.uniforms.count("vibranceValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("vibranceValue").location, *op.vibranceVal);
 | 
						|
                    if (op.saturationVal && op.uniforms.count("saturationValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("saturationValue").location, *op.saturationVal);
 | 
						|
                    if (op.clarityVal && op.uniforms.count("clarityValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("clarityValue").location, *op.clarityVal);
 | 
						|
                    if (op.textureVal && op.uniforms.count("textureValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("textureValue").location, *op.textureVal);
 | 
						|
                    if (op.dehazeVal && op.uniforms.count("dehazeValue"))
 | 
						|
                        glUniform1f(op.uniforms.at("dehazeValue").location, *op.dehazeVal);
 | 
						|
                    // ... etc ...
 | 
						|
                }
 | 
						|
 | 
						|
                glDrawArrays(GL_TRIANGLES, 0, 6);
 | 
						|
 | 
						|
                // Update read texture for the *next* step
 | 
						|
                currentReadTexId = m_tex[currentBufferIndex];
 | 
						|
 | 
						|
                // <<< DIFF CAPTURE: After processing the target operation >>>
 | 
						|
                if (diffOpIndexOpt && (int)i == *diffOpIndexOpt)
 | 
						|
                {
 | 
						|
                    m_diffTexAfter = currentReadTexId; // Capture the output of this step
 | 
						|
                    printf("Pipeline: Capturing TexID %u as 'After' for diff op '%s'\n", m_diffTexAfter, op.name.c_str());
 | 
						|
                }
 | 
						|
 | 
						|
                // Swap write target for the *next* step
 | 
						|
                currentBufferIndex = 1 - currentBufferIndex;
 | 
						|
                appliedOpsCount++;
 | 
						|
                handledInitialStep = true;
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        // At this point, `currentReadTexId` holds the final linear result of the pipeline
 | 
						|
        result.linearOutput = currentReadTexId;
 | 
						|
        printf("Pipeline: Finished editing ops. Final linear TexID: %u\n", result.linearOutput);
 | 
						|
 | 
						|
        // --- Output Color Space Conversion (for display, using linearOutput) ---
 | 
						|
        result.displayOutput = result.linearOutput;                    // Assume linear output initially
 | 
						|
        GLuint displayConversionTargetFbo = m_fbo[currentBufferIndex]; // The *next* buffer is free
 | 
						|
        bool performedDisplayConversion = false;
 | 
						|
 | 
						|
        if (outputColorSpace == ColorSpace::SRGB)
 | 
						|
        {
 | 
						|
            bool needsLinearToSrgb = false;
 | 
						|
            // ... (logic to determine if conversion is needed, same as before) ...
 | 
						|
            if (handledInitialStep || appliedOpsCount > 0)
 | 
						|
                needsLinearToSrgb = true;
 | 
						|
            else
 | 
						|
                needsLinearToSrgb = (inputColorSpace == ColorSpace::LINEAR_SRGB);
 | 
						|
 | 
						|
            if (needsLinearToSrgb)
 | 
						|
            {
 | 
						|
                printf("Pipeline: Applying Linear -> sRGB conversion for display output.\n");
 | 
						|
                glBindFramebuffer(GL_FRAMEBUFFER, displayConversionTargetFbo); // Use the *next* FBO
 | 
						|
                glUseProgram(m_linearToSrgbShader);
 | 
						|
                glActiveTexture(GL_TEXTURE0);
 | 
						|
                glBindTexture(GL_TEXTURE_2D, result.linearOutput); // Read the final linear result
 | 
						|
                glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0);
 | 
						|
                glDrawArrays(GL_TRIANGLES, 0, 6);
 | 
						|
                result.displayOutput = m_tex[currentBufferIndex]; // Result is in the target FBO's texture
 | 
						|
                performedDisplayConversion = true;
 | 
						|
                printf("Pipeline: Final display (sRGB) TexID: %u\n", result.displayOutput);
 | 
						|
            }
 | 
						|
        }
 | 
						|
        // If no conversion needed, result.displayOutput remains == result.linearOutput
 | 
						|
 | 
						|
        // --- Diff Rendering Pass (if active) ---
 | 
						|
        if (m_diffActive && diffOpIndexOpt && m_diffTexBefore != 0 && m_diffTexAfter != 0)
 | 
						|
        {
 | 
						|
            printf("Pipeline: Performing Diff Pass (Before: %u, After: %u)\n", m_diffTexBefore, m_diffTexAfter);
 | 
						|
            // Determine where to render the diff output.
 | 
						|
            // We need a *different* buffer than the one holding the final display output.
 | 
						|
            // If display conversion happened, 'currentBufferIndex' points to the FBO used for it.
 | 
						|
            // The *other* FBO (1-currentBufferIndex) should hold the final linear result.
 | 
						|
            // Let's render the diff into the FBO that holds the linear result, overwriting it temporarily for display.
 | 
						|
            // Or, better: if display conversion happened, render to the *next* buffer index again. If not, use the first buffer.
 | 
						|
            GLuint diffTargetFbo;
 | 
						|
            GLuint diffTargetTexId;
 | 
						|
            if (performedDisplayConversion)
 | 
						|
            {
 | 
						|
                // Display output is in m_tex[currentBufferIndex].
 | 
						|
                // Linear output is in m_tex[1-currentBufferIndex].
 | 
						|
                // Render diff to the *next* buffer again (overwriting linear temporarily *if needed*)
 | 
						|
                int diffBufferIndex = 1 - currentBufferIndex; // The buffer holding linear result
 | 
						|
                diffTargetFbo = m_fbo[diffBufferIndex];
 | 
						|
                diffTargetTexId = m_tex[diffBufferIndex];
 | 
						|
                printf("Pipeline: Rendering Diff to FBO %d (TexID %d) - buffer that held linear.\n", diffTargetFbo, diffTargetTexId);
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                // Display output == Linear output, both are currentReadTexId.
 | 
						|
                // Render diff to the *next* buffer index.
 | 
						|
                diffTargetFbo = m_fbo[currentBufferIndex];
 | 
						|
                diffTargetTexId = m_tex[currentBufferIndex];
 | 
						|
                printf("Pipeline: Rendering Diff to FBO %d (TexID %d) - next available buffer.\n", diffTargetFbo, diffTargetTexId);
 | 
						|
            }
 | 
						|
 | 
						|
            glBindFramebuffer(GL_FRAMEBUFFER, diffTargetFbo);
 | 
						|
            glUseProgram(m_diffShader);
 | 
						|
 | 
						|
            // Bind "Before" texture to unit 0
 | 
						|
            glActiveTexture(GL_TEXTURE0);
 | 
						|
            glBindTexture(GL_TEXTURE_2D, m_diffTexBefore);
 | 
						|
            glUniform1i(glGetUniformLocation(m_diffShader, "texBefore"), 0);
 | 
						|
 | 
						|
            // Bind "After" texture to unit 1
 | 
						|
            glActiveTexture(GL_TEXTURE1);
 | 
						|
            glBindTexture(GL_TEXTURE_2D, m_diffTexAfter);
 | 
						|
            glUniform1i(glGetUniformLocation(m_diffShader, "texAfter"), 1);
 | 
						|
 | 
						|
            // Set other diff uniforms (e.g., boost)
 | 
						|
            glUniform1f(glGetUniformLocation(m_diffShader, "diffBoost"), 5.0f); // Example boost
 | 
						|
 | 
						|
            glDrawArrays(GL_TRIANGLES, 0, 6);
 | 
						|
 | 
						|
            result.diffOutput = diffTargetTexId; // Store the ID of the texture containing the diff
 | 
						|
            printf("Pipeline: Diff rendering complete. Diff TexID: %u\n", result.diffOutput);
 | 
						|
 | 
						|
            // IMPORTANT: Ensure texture unit 1 is unbound afterwards if necessary
 | 
						|
            glActiveTexture(GL_TEXTURE1);
 | 
						|
            glBindTexture(GL_TEXTURE_2D, 0);
 | 
						|
            glActiveTexture(GL_TEXTURE0); // Return to default active texture unit
 | 
						|
        }
 | 
						|
 | 
						|
        // --- Cleanup ---
 | 
						|
        glBindVertexArray(lastVao);
 | 
						|
        glBindFramebuffer(GL_FRAMEBUFFER, lastFBO);
 | 
						|
        glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
 | 
						|
        glUseProgram(lastProgram);
 | 
						|
        glActiveTexture(lastActiveTexture);
 | 
						|
        glBindTexture(GL_TEXTURE_2D, lastBoundTexture);
 | 
						|
 | 
						|
        return result;
 | 
						|
    }
 | 
						|
 | 
						|
public:
 | 
						|
    // The ordered list of operations the user has configured
 | 
						|
    std::vector<PipelineOperation> activeOperations;
 | 
						|
    ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default
 | 
						|
    ColorSpace outputColorSpace = ColorSpace::SRGB;       // Default for display
 | 
						|
 | 
						|
    ImageProcessingPipeline() = default;
 | 
						|
 | 
						|
    ~ImageProcessingPipeline()
 | 
						|
    {
 | 
						|
        DestroyFBOs(); // Cleanup FBOs/Textures
 | 
						|
        if (m_vao)
 | 
						|
            glDeleteVertexArrays(1, &m_vao);
 | 
						|
        if (m_vbo)
 | 
						|
            glDeleteBuffers(1, &m_vbo);
 | 
						|
        // Shaders owned by PipelineOperation structs should be deleted externally or via smart pointers
 | 
						|
        // (Assuming InitShaderOperations handles this)
 | 
						|
        if (m_passthroughShader)
 | 
						|
            glDeleteProgram(m_passthroughShader);
 | 
						|
        if (m_linearToSrgbShader)
 | 
						|
            glDeleteProgram(m_linearToSrgbShader);
 | 
						|
        if (m_srgbToLinearShader)
 | 
						|
            glDeleteProgram(m_srgbToLinearShader);
 | 
						|
        if (m_diffShader)
 | 
						|
            glDeleteProgram(m_diffShader); // <<< Cleanup diff shader
 | 
						|
        printf("ImageProcessingPipeline destroyed.\n");
 | 
						|
    }
 | 
						|
 | 
						|
    void Init(const std::string &shaderBasePath)
 | 
						|
    {
 | 
						|
        printf("Initializing ImageProcessingPipeline...\n");
 | 
						|
        CreateFullscreenQuad();
 | 
						|
 | 
						|
        std::string vsPath = shaderBasePath + "passthrough.vert";
 | 
						|
        printf("Loading essential shaders from: %s\n", shaderBasePath.c_str());
 | 
						|
        m_passthroughShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "passthrough.frag");
 | 
						|
        m_linearToSrgbShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "linear_to_srgb.frag");
 | 
						|
        m_srgbToLinearShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "srgb_to_linear.frag");
 | 
						|
        m_diffShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "diff.frag"); // <<< Load diff shader
 | 
						|
 | 
						|
        if (!m_passthroughShader || !m_linearToSrgbShader || !m_srgbToLinearShader)
 | 
						|
        {
 | 
						|
            fprintf(stderr, "FATAL: Failed to load essential pipeline shaders! Check paths and shader code.\n");
 | 
						|
            // Consider throwing or setting an error state
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            printf("Essential pipeline shaders loaded (Passthrough: %u, Lin->sRGB: %u, sRGB->Lin: %u).\n",
 | 
						|
                   m_passthroughShader, m_linearToSrgbShader, m_srgbToLinearShader);
 | 
						|
        }
 | 
						|
        MarkDirty(); // Ensure processing happens on first frame after init
 | 
						|
    }
 | 
						|
 | 
						|
    // Call this to signal that parameters have changed and reprocessing is needed
 | 
						|
    void MarkDirty()
 | 
						|
    {
 | 
						|
        if (!m_isDirty)
 | 
						|
        {
 | 
						|
            printf("Pipeline: Marked as dirty.\n");
 | 
						|
            m_isDirty = true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    bool IsDirty() const
 | 
						|
    {
 | 
						|
        return m_isDirty;
 | 
						|
    }
 | 
						|
 | 
						|
    void ResetResources()
 | 
						|
    {
 | 
						|
        printf("Pipeline: Resetting FBOs and Textures.\n");
 | 
						|
        DestroyFBOs(); // Call the existing cleanup method
 | 
						|
    }
 | 
						|
 | 
						|
    // --- Diffing Control ---
 | 
						|
    void SetDiffActiveOperation(const char *operationName)
 | 
						|
    {
 | 
						|
        std::string nameStr = (operationName ? operationName : "");
 | 
						|
        bool wasActive = m_diffActive;
 | 
						|
        std::string oldName = m_diffActiveOperationName;
 | 
						|
 | 
						|
        m_diffActive = !nameStr.empty();
 | 
						|
        m_diffActiveOperationName = nameStr;
 | 
						|
 | 
						|
        if (m_diffActive != wasActive || m_diffActiveOperationName != oldName)
 | 
						|
        {
 | 
						|
            printf("Pipeline: Diff state changed. Active: %s, Op: %s\n",
 | 
						|
                   m_diffActive ? "Yes" : "No", m_diffActiveOperationName.c_str());
 | 
						|
            MarkDirty(); // Changing diff state requires reprocessing
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    bool IsDiffActive() const
 | 
						|
    {
 | 
						|
        return m_diffActive;
 | 
						|
    }
 | 
						|
 | 
						|
    bool IsDiffActiveForOperation(const char *operationName) const
 | 
						|
    {
 | 
						|
        if (!operationName)
 | 
						|
            return false;
 | 
						|
        return m_diffActive && m_diffActiveOperationName == operationName;
 | 
						|
    }
 | 
						|
 | 
						|
    // --- Public Interface ---
 | 
						|
    // Gets the texture ID for display or saving, processing only if necessary.
 | 
						|
    GLuint GetProcessedTexture(GLuint inputTextureId, int width, int height, bool forDisplay)
 | 
						|
    {
 | 
						|
        if (inputTextureId == 0 || width <= 0 || height <= 0)
 | 
						|
        {
 | 
						|
            // ... (invalid input handling, clear cache) ...
 | 
						|
            return 0;
 | 
						|
        }
 | 
						|
 | 
						|
        // Cache validity check (same as before)
 | 
						|
        bool cacheInputValid = (inputTextureId == m_lastProcessedInputTextureId &&
 | 
						|
                                width == m_cachedInputWidth &&
 | 
						|
                                height == m_cachedInputHeight);
 | 
						|
 | 
						|
        // Cache is invalidated if dirty OR input changed OR diff state is active (diff result isn't cached long-term)
 | 
						|
        if (!m_isDirty && cacheInputValid && !m_diffActive && m_cachedDisplayTextureId != 0 && m_cachedLinearTextureId != 0)
 | 
						|
        {
 | 
						|
            // Cache hit and not diffing
 | 
						|
            return forDisplay ? m_cachedDisplayTextureId : m_cachedLinearTextureId;
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            // Needs processing
 | 
						|
            if (m_isDirty)
 | 
						|
                printf("Pipeline: Cache dirty flag set. Reprocessing.\n");
 | 
						|
            if (!cacheInputValid)
 | 
						|
                printf("Pipeline: Input changed. Reprocessing.\n");
 | 
						|
            if (m_diffActive)
 | 
						|
                printf("Pipeline: Diff mode active. Reprocessing for diff.\n");
 | 
						|
 | 
						|
            ProcessingResult results = ExecuteProcessingSteps(inputTextureId, width, height);
 | 
						|
 | 
						|
            if (results.linearOutput != 0 && results.displayOutput != 0)
 | 
						|
            {
 | 
						|
                printf("Pipeline: Processing successful.\n");
 | 
						|
                // Update cache ONLY if NOT in diff mode
 | 
						|
                if (!m_diffActive)
 | 
						|
                {
 | 
						|
                    printf("Pipeline: Updating cache (Linear: %u, Display: %u).\n", results.linearOutput, results.displayOutput);
 | 
						|
                    m_cachedLinearTextureId = results.linearOutput;
 | 
						|
                    m_cachedDisplayTextureId = results.displayOutput;
 | 
						|
                    m_cachedInputWidth = width;
 | 
						|
                    m_cachedInputHeight = height;
 | 
						|
                    m_lastProcessedInputTextureId = inputTextureId;
 | 
						|
                    m_isDirty = false; // Clear dirty flag *after* successful non-diff processing
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    printf("Pipeline: Diff active, cache not updated.\n");
 | 
						|
                    // Keep pipeline dirty if diffing, so next non-diff frame recalculates correctly
 | 
						|
                    m_isDirty = true;
 | 
						|
                }
 | 
						|
 | 
						|
                // Return the correct texture
 | 
						|
                if (forDisplay)
 | 
						|
                {
 | 
						|
                    // If diffing was active and produced a result, show that
 | 
						|
                    if (m_diffActive && results.diffOutput != 0)
 | 
						|
                    {
 | 
						|
                        printf("Pipeline: Returning DIFF texture (%u) for display.\n", results.diffOutput);
 | 
						|
                        return results.diffOutput;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        // Otherwise, return the normal display texture
 | 
						|
                        printf("Pipeline: Returning normal display texture (%u) for display.\n", results.displayOutput);
 | 
						|
                        return results.displayOutput;
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    // For saving, always return the linear result
 | 
						|
                    printf("Pipeline: Returning LINEAR texture (%u) for saving.\n", results.linearOutput);
 | 
						|
                    return results.linearOutput;
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                // Processing failed
 | 
						|
                fprintf(stderr, "Pipeline: ExecuteProcessingSteps failed. Invalidating cache.\n");
 | 
						|
                m_cachedDisplayTextureId = 0;
 | 
						|
                m_cachedLinearTextureId = 0;
 | 
						|
                m_cachedInputWidth = 0;
 | 
						|
                m_cachedInputHeight = 0;
 | 
						|
                m_lastProcessedInputTextureId = 0;
 | 
						|
                m_isDirty = true; // Remain dirty
 | 
						|
                return 0;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
};
 | 
						|
 | 
						|
static ImageProcessingPipeline
 | 
						|
    g_pipeline; // <<< Global pipeline manager instance
 | 
						|
static std::vector<std::unique_ptr<PipelineOperation>>
 | 
						|
    g_allOperations;                    // Store all possible operations
 | 
						|
static GLuint g_processedTextureId = 0; // Texture ID after pipeline processing
 | 
						|
static ColorSpace g_inputColorSpace =
 | 
						|
    ColorSpace::LINEAR_SRGB; // Connect to pipeline's setting
 | 
						|
static ColorSpace g_outputColorSpace =
 | 
						|
    ColorSpace::SRGB; // Connect to pipeline's setting
 | 
						|
 | 
						|
// File Dialogs
 | 
						|
static ImGui::FileBrowser g_openFileDialog;
 | 
						|
// Add flags for save dialog: Allow new filename, allow creating directories
 | 
						|
static ImGui::FileBrowser
 | 
						|
    g_exportSaveFileDialog(ImGuiFileBrowserFlags_EnterNewFilename |
 | 
						|
                           ImGuiFileBrowserFlags_CreateNewDir);
 | 
						|
 | 
						|
// Export Dialog State
 | 
						|
static bool g_showExportWindow = false;
 | 
						|
static ImageSaveFormat g_exportFormat = ImageSaveFormat::JPEG; // Default format
 | 
						|
static int g_exportQuality = 90;                               // Default JPEG quality
 | 
						|
static std::string g_exportErrorMsg =
 | 
						|
    ""; // To display errors in the export dialog
 | 
						|
 | 
						|
// Current loaded file path (useful for default export name)
 | 
						|
static std::string g_currentFilePath = "";
 | 
						|
 | 
						|
// Crop State
 | 
						|
static bool g_cropActive = false;
 | 
						|
static ImVec4 g_cropRectNorm =
 | 
						|
    ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // (MinX, MinY, MaxX, MaxY) normalized 0-1
 | 
						|
static ImVec4 g_cropRectNormInitial =
 | 
						|
    g_cropRectNorm; // Store initial state for cancel/dragging base
 | 
						|
static float g_cropAspectRatio =
 | 
						|
    0.0f;                                  // 0.0f = Freeform, > 0.0f = constrained (Width / Height)
 | 
						|
static int g_selectedAspectRatioIndex = 0; // Index for the dropdown
 | 
						|
 | 
						|
static GLuint g_histogramComputeShader = 0;
 | 
						|
static GLuint g_histogramSSBO = 0;
 | 
						|
const int NUM_HISTOGRAM_BINS = 256;
 | 
						|
const int HISTOGRAM_BUFFER_SIZE = NUM_HISTOGRAM_BINS * 3; // R, G, B
 | 
						|
static std::vector<unsigned int> g_histogramDataCPU(HISTOGRAM_BUFFER_SIZE, 0);
 | 
						|
static unsigned int g_histogramMaxCount =
 | 
						|
    255; // Max count found, for scaling (init to 1 to avoid div by zero)
 | 
						|
static bool g_histogramResourcesInitialized = false;
 | 
						|
 | 
						|
// Interaction state
 | 
						|
enum class CropHandle
 | 
						|
{
 | 
						|
    NONE,
 | 
						|
    TOP_LEFT,
 | 
						|
    TOP_RIGHT,
 | 
						|
    BOTTOM_LEFT,
 | 
						|
    BOTTOM_RIGHT,
 | 
						|
    TOP,
 | 
						|
    BOTTOM,
 | 
						|
    LEFT,
 | 
						|
    RIGHT,
 | 
						|
    INSIDE
 | 
						|
};
 | 
						|
static CropHandle g_activeCropHandle = CropHandle::NONE;
 | 
						|
static bool g_isDraggingCrop = false;
 | 
						|
static ImVec2 g_dragStartMousePos = ImVec2(0, 0); // Screen coords
 | 
						|
 | 
						|
bool InitHistogramResources(const std::string &shaderBasePath)
 | 
						|
{
 | 
						|
    printf("Initializing Histogram Resources...\n");
 | 
						|
    // Load Compute Shader
 | 
						|
    // We need a way to load compute shaders, modify shader_utils or add here
 | 
						|
    std::string compSource =
 | 
						|
        ReadFile(shaderBasePath + "histogram.comp"); // Assuming ReadFile exists
 | 
						|
    if (compSource.empty())
 | 
						|
    {
 | 
						|
        fprintf(stderr, "ERROR: Failed to read histogram.comp\n");
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    // Simple Compute Shader Compilation/Linking (add error checking!)
 | 
						|
    GLuint computeShaderObj = glCreateShader(GL_COMPUTE_SHADER);
 | 
						|
    const char *src = compSource.c_str();
 | 
						|
    glShaderSource(computeShaderObj, 1, &src, nullptr);
 | 
						|
    glCompileShader(computeShaderObj);
 | 
						|
    // --- Add GLint success; glGetShaderiv; glGetShaderInfoLog checks ---
 | 
						|
    GLint success;
 | 
						|
    glGetShaderiv(computeShaderObj, GL_COMPILE_STATUS, &success);
 | 
						|
    if (!success)
 | 
						|
    {
 | 
						|
        GLint logLength;
 | 
						|
        glGetShaderiv(computeShaderObj, GL_INFO_LOG_LENGTH, &logLength);
 | 
						|
        std::vector<char> log(logLength);
 | 
						|
        glGetShaderInfoLog(computeShaderObj, logLength, nullptr, log.data());
 | 
						|
        fprintf(stderr, "ERROR::SHADER::HISTOGRAM::COMPILATION_FAILED\n%s\n",
 | 
						|
                log.data());
 | 
						|
        glDeleteShader(computeShaderObj);
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    g_histogramComputeShader = glCreateProgram();
 | 
						|
    glAttachShader(g_histogramComputeShader, computeShaderObj);
 | 
						|
    glLinkProgram(g_histogramComputeShader);
 | 
						|
    // --- Add GLint success; glGetProgramiv; glGetProgramInfoLog checks ---
 | 
						|
    glGetProgramiv(g_histogramComputeShader, GL_LINK_STATUS, &success);
 | 
						|
    if (!success)
 | 
						|
    {
 | 
						|
        GLint logLength;
 | 
						|
        glGetProgramiv(g_histogramComputeShader, GL_INFO_LOG_LENGTH, &logLength);
 | 
						|
        std::vector<char> log(logLength);
 | 
						|
        glGetProgramInfoLog(g_histogramComputeShader, logLength, nullptr,
 | 
						|
                            log.data());
 | 
						|
        fprintf(stderr, "ERROR::PROGRAM::HISTOGRAM::LINKING_FAILED\n%s\n",
 | 
						|
                log.data());
 | 
						|
        glDeleteProgram(g_histogramComputeShader);
 | 
						|
        g_histogramComputeShader = 0;
 | 
						|
        glDeleteShader(computeShaderObj); // Delete shader obj even on link failure
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    glDeleteShader(computeShaderObj); // Delete shader object after linking
 | 
						|
    printf("Histogram compute shader loaded and linked successfully (Program ID: "
 | 
						|
           "%u).\n",
 | 
						|
           g_histogramComputeShader);
 | 
						|
 | 
						|
    // Create Shader Storage Buffer Object (SSBO)
 | 
						|
    glGenBuffers(1, &g_histogramSSBO);
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
 | 
						|
    // Allocate buffer size: 3 channels * 256 bins * size of uint
 | 
						|
    glBufferData(GL_SHADER_STORAGE_BUFFER,
 | 
						|
                 HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int), NULL,
 | 
						|
                 GL_DYNAMIC_READ);             // Data will be written by GPU, read by CPU
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
 | 
						|
 | 
						|
    GLenum err = glGetError();
 | 
						|
    if (err != GL_NO_ERROR || g_histogramSSBO == 0)
 | 
						|
    {
 | 
						|
        fprintf(stderr,
 | 
						|
                "ERROR: Failed to create histogram SSBO. OpenGL Error: %u\n", err);
 | 
						|
        if (g_histogramComputeShader)
 | 
						|
            glDeleteProgram(g_histogramComputeShader);
 | 
						|
        g_histogramComputeShader = 0;
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        printf("Histogram SSBO created successfully (Buffer ID: %u, Size: %d "
 | 
						|
               "bytes).\n",
 | 
						|
               g_histogramSSBO, HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int));
 | 
						|
    }
 | 
						|
 | 
						|
    g_histogramResourcesInitialized = true;
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
// Aspect Ratio Options
 | 
						|
struct AspectRatioOption
 | 
						|
{
 | 
						|
    const char *name;
 | 
						|
    float ratio; // W/H
 | 
						|
};
 | 
						|
static std::vector<AspectRatioOption> g_aspectRatios = {
 | 
						|
    {"Freeform", 0.0f}, {"Original", 0.0f}, // Will be calculated dynamically
 | 
						|
    {"1:1", 1.0f},
 | 
						|
    {"16:9", 16.0f / 9.0f},
 | 
						|
    {"9:16", 9.0f / 16.0f},
 | 
						|
    {"4:3", 4.0f / 3.0f},
 | 
						|
    {"3:4", 3.0f / 4.0f},
 | 
						|
    // Add more as needed
 | 
						|
};
 | 
						|
 | 
						|
void UpdateCropRect(ImVec4 &rectNorm, CropHandle handle, ImVec2 deltaNorm,
 | 
						|
                    float aspectRatio)
 | 
						|
{
 | 
						|
    ImVec2 minXY = ImVec2(rectNorm.x, rectNorm.y);
 | 
						|
    ImVec2 maxXY = ImVec2(rectNorm.z, rectNorm.w);
 | 
						|
 | 
						|
    // Apply delta based on handle
 | 
						|
    switch (handle)
 | 
						|
    {
 | 
						|
    case CropHandle::TOP_LEFT:
 | 
						|
        minXY += deltaNorm;
 | 
						|
        break;
 | 
						|
    case CropHandle::TOP_RIGHT:
 | 
						|
        minXY.y += deltaNorm.y;
 | 
						|
        maxXY.x += deltaNorm.x;
 | 
						|
        break;
 | 
						|
    case CropHandle::BOTTOM_LEFT:
 | 
						|
        minXY.x += deltaNorm.x;
 | 
						|
        maxXY.y += deltaNorm.y;
 | 
						|
        break;
 | 
						|
    case CropHandle::BOTTOM_RIGHT:
 | 
						|
        maxXY += deltaNorm;
 | 
						|
        break;
 | 
						|
    case CropHandle::TOP:
 | 
						|
        minXY.y += deltaNorm.y;
 | 
						|
        break;
 | 
						|
    case CropHandle::BOTTOM:
 | 
						|
        maxXY.y += deltaNorm.y;
 | 
						|
        break;
 | 
						|
    case CropHandle::LEFT:
 | 
						|
        minXY.x += deltaNorm.x;
 | 
						|
        break;
 | 
						|
    case CropHandle::RIGHT:
 | 
						|
        maxXY.x += deltaNorm.x;
 | 
						|
        break;
 | 
						|
    case CropHandle::INSIDE:
 | 
						|
        minXY += deltaNorm;
 | 
						|
        maxXY += deltaNorm;
 | 
						|
        break;
 | 
						|
    case CropHandle::NONE:
 | 
						|
        return; // No change
 | 
						|
    }
 | 
						|
 | 
						|
    // Ensure min < max temporarily before aspect constraint
 | 
						|
    if (minXY.x > maxXY.x)
 | 
						|
        ImSwap(minXY.x, maxXY.x);
 | 
						|
    if (minXY.y > maxXY.y)
 | 
						|
        ImSwap(minXY.y, maxXY.y);
 | 
						|
 | 
						|
    // Apply Aspect Ratio Constraint (if aspectRatio > 0)
 | 
						|
    if (aspectRatio > 0.0f && handle != CropHandle::INSIDE &&
 | 
						|
        handle != CropHandle::NONE)
 | 
						|
    {
 | 
						|
        float currentW = maxXY.x - minXY.x;
 | 
						|
        float currentH = maxXY.y - minXY.y;
 | 
						|
 | 
						|
        if (currentW < 1e-5f)
 | 
						|
            currentW = 1e-5f; // Avoid division by zero
 | 
						|
        if (currentH < 1e-5f)
 | 
						|
            currentH = 1e-5f;
 | 
						|
 | 
						|
        float currentAspect = currentW / currentH;
 | 
						|
        float targetAspect = aspectRatio;
 | 
						|
 | 
						|
        // Determine which dimension to adjust based on which handle was moved and
 | 
						|
        // aspect delta Simplified approach: Adjust height based on width, unless
 | 
						|
        // moving top/bottom handles primarily
 | 
						|
        bool adjustHeight = true;
 | 
						|
        if (handle == CropHandle::TOP || handle == CropHandle::BOTTOM)
 | 
						|
        {
 | 
						|
            adjustHeight = false; // Primarily adjust width based on height change
 | 
						|
        }
 | 
						|
 | 
						|
        if (adjustHeight)
 | 
						|
        { // Adjust height based on width
 | 
						|
            float targetH = currentW / targetAspect;
 | 
						|
            float deltaH = targetH - currentH;
 | 
						|
            // Distribute height change based on handle
 | 
						|
            if (handle == CropHandle::TOP_LEFT || handle == CropHandle::TOP_RIGHT ||
 | 
						|
                handle == CropHandle::TOP)
 | 
						|
            {
 | 
						|
                minXY.y -= deltaH; // Adjust top edge
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                maxXY.y += deltaH; // Adjust bottom edge (or split for side handles?)
 | 
						|
                                   // For LEFT/RIGHT handles, could split deltaH: minXY.y -= deltaH*0.5;
 | 
						|
                                   // maxXY.y += deltaH*0.5;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else
 | 
						|
        { // Adjust width based on height
 | 
						|
            float targetW = currentH * targetAspect;
 | 
						|
            float deltaW = targetW - currentW;
 | 
						|
            // Distribute width change based on handle
 | 
						|
            if (handle == CropHandle::TOP_LEFT || handle == CropHandle::BOTTOM_LEFT ||
 | 
						|
                handle == CropHandle::LEFT)
 | 
						|
            {
 | 
						|
                minXY.x -= deltaW; // Adjust left edge
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                maxXY.x += deltaW; // Adjust right edge
 | 
						|
                                   // For TOP/BOTTOM handles, could split deltaW: minXY.x -= deltaW*0.5;
 | 
						|
                                   // maxXY.x += deltaW*0.5;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    } // End aspect ratio constraint
 | 
						|
 | 
						|
    // Update the output rectNorm
 | 
						|
    rectNorm = ImVec4(minXY.x, minXY.y, maxXY.x, maxXY.y);
 | 
						|
}
 | 
						|
 | 
						|
// Helper function to crop AppImage data
 | 
						|
bool ApplyCropToImage(AppImage &image, const ImVec4 cropRectNorm)
 | 
						|
{
 | 
						|
    if (image.isEmpty())
 | 
						|
    {
 | 
						|
        fprintf(stderr, "ApplyCropToImage: Input image is empty.\n");
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    if (cropRectNorm.x >= cropRectNorm.z || cropRectNorm.y >= cropRectNorm.w)
 | 
						|
    {
 | 
						|
        fprintf(
 | 
						|
            stderr,
 | 
						|
            "ApplyCropToImage: Invalid crop rectangle (zero or negative size).\n");
 | 
						|
        return false; // Invalid crop rect
 | 
						|
    }
 | 
						|
 | 
						|
    // Clamp rect just in case
 | 
						|
    ImVec4 clampedRect = cropRectNorm;
 | 
						|
    clampedRect.x = ImClamp(clampedRect.x, 0.0f, 1.0f);
 | 
						|
    clampedRect.y = ImClamp(clampedRect.y, 0.0f, 1.0f);
 | 
						|
    clampedRect.z = ImClamp(clampedRect.z, 0.0f, 1.0f);
 | 
						|
    clampedRect.w = ImClamp(clampedRect.w, 0.0f, 1.0f);
 | 
						|
 | 
						|
    // Calculate pixel coordinates
 | 
						|
    int srcW = image.getWidth();
 | 
						|
    int srcH = image.getHeight();
 | 
						|
    int channels = image.getChannels();
 | 
						|
 | 
						|
    int cropX_px = static_cast<int>(round(clampedRect.x * srcW));
 | 
						|
    int cropY_px = static_cast<int>(round(clampedRect.y * srcH));
 | 
						|
    int cropMaxX_px = static_cast<int>(round(clampedRect.z * srcW));
 | 
						|
    int cropMaxY_px = static_cast<int>(round(clampedRect.w * srcH));
 | 
						|
 | 
						|
    int cropW_px = cropMaxX_px - cropX_px;
 | 
						|
    int cropH_px = cropMaxY_px - cropY_px;
 | 
						|
 | 
						|
    if (cropW_px <= 0 || cropH_px <= 0)
 | 
						|
    {
 | 
						|
        fprintf(
 | 
						|
            stderr,
 | 
						|
            "ApplyCropToImage: Resulting crop size is zero or negative (%dx%d).\n",
 | 
						|
            cropW_px, cropH_px);
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    printf("Applying crop: Start=(%d,%d), Size=(%dx%d)\n", cropX_px, cropY_px,
 | 
						|
           cropW_px, cropH_px);
 | 
						|
 | 
						|
    // Create new image for cropped data
 | 
						|
    AppImage croppedImage(cropW_px, cropH_px, channels);
 | 
						|
    if (croppedImage.isEmpty())
 | 
						|
    {
 | 
						|
        fprintf(stderr,
 | 
						|
                "ApplyCropToImage: Failed to allocate memory for cropped image.\n");
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
    croppedImage.m_isLinear = image.isLinear(); // Preserve flags
 | 
						|
    croppedImage.m_colorSpaceName = image.getColorSpaceName();
 | 
						|
    // TODO: Copy metadata/ICC profile if needed? Cropping usually invalidates
 | 
						|
    // some metadata.
 | 
						|
 | 
						|
    const float *srcData = image.getData();
 | 
						|
    float *dstData = croppedImage.getData();
 | 
						|
 | 
						|
    // Copy pixel data row by row, channel by channel
 | 
						|
    for (int y_dst = 0; y_dst < cropH_px; ++y_dst)
 | 
						|
    {
 | 
						|
        int y_src = cropY_px + y_dst;
 | 
						|
        // Ensure source Y is valid (should be due to clamping/checks, but be safe)
 | 
						|
        if (y_src < 0 || y_src >= srcH)
 | 
						|
            continue;
 | 
						|
 | 
						|
        // Calculate start pointers for source and destination rows
 | 
						|
        const float *srcRowStart =
 | 
						|
            srcData + (static_cast<size_t>(y_src) * srcW + cropX_px) * channels;
 | 
						|
        float *dstRowStart =
 | 
						|
            dstData + (static_cast<size_t>(y_dst) * cropW_px) * channels;
 | 
						|
 | 
						|
        // Copy the entire row (width * channels floats)
 | 
						|
        std::memcpy(dstRowStart, srcRowStart,
 | 
						|
                    static_cast<size_t>(cropW_px) * channels * sizeof(float));
 | 
						|
    }
 | 
						|
 | 
						|
    // Replace the original image data with the cropped data
 | 
						|
    // Use std::move if AppImage supports move assignment for efficiency
 | 
						|
    image = std::move(croppedImage);
 | 
						|
 | 
						|
    printf("Cropped image created successfully (%dx%d).\n", image.getWidth(),
 | 
						|
           image.getHeight());
 | 
						|
    return true;
 | 
						|
}
 | 
						|
 | 
						|
void InitShaderOperations(const std::string &shaderBasePath)
 | 
						|
{
 | 
						|
    // Clear existing (if any)
 | 
						|
    g_allOperations.clear();
 | 
						|
    g_pipeline.activeOperations
 | 
						|
        .clear(); // Also clear the active list in the pipeline
 | 
						|
 | 
						|
    // --- Define Operations ---
 | 
						|
    // Use unique_ptr for automatic memory management
 | 
						|
    // Match uniform names to the GLSL shaders
 | 
						|
 | 
						|
    auto whiteBalanceOp = std::make_unique<PipelineOperation>("White Balance");
 | 
						|
    whiteBalanceOp->shaderProgram =
 | 
						|
        LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert",
 | 
						|
                                   shaderBasePath + "white_balance.frag");
 | 
						|
    if (whiteBalanceOp->shaderProgram)
 | 
						|
    {
 | 
						|
        whiteBalanceOp->uniforms["temperatureValue"] = {"temperature"};
 | 
						|
        whiteBalanceOp->uniforms["tintValue"] = {"tint"};
 | 
						|
        whiteBalanceOp->temperatureVal = &temperature;
 | 
						|
        whiteBalanceOp->tintVal = ∭
 | 
						|
        whiteBalanceOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(whiteBalanceOp));
 | 
						|
        printf("  + Loaded White Balance\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED White Balance\n");
 | 
						|
 | 
						|
    auto exposureOp = std::make_unique<PipelineOperation>("Exposure");
 | 
						|
    exposureOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "exposure.frag");
 | 
						|
    exposureOp->uniforms["exposureValue"] = {"exposureValue"};
 | 
						|
    exposureOp->exposureVal = &exposure; // Link to global slider variable
 | 
						|
    exposureOp->FindUniformLocations();
 | 
						|
    g_allOperations.push_back(std::move(exposureOp));
 | 
						|
 | 
						|
    auto contrastOp = std::make_unique<PipelineOperation>("Contrast");
 | 
						|
    contrastOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "contrast.frag");
 | 
						|
    if (contrastOp->shaderProgram)
 | 
						|
    {
 | 
						|
        contrastOp->uniforms["contrastValue"] = {"contrastValue"};
 | 
						|
        contrastOp->contrastVal = &contrast;
 | 
						|
        contrastOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(contrastOp));
 | 
						|
        printf("  + Loaded Contrast\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Contrast\n");
 | 
						|
 | 
						|
    auto highlightsShadowsOp =
 | 
						|
        std::make_unique<PipelineOperation>("Highlights/Shadows");
 | 
						|
    highlightsShadowsOp->shaderProgram =
 | 
						|
        LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert",
 | 
						|
                                   shaderBasePath + "highlights_shadows.frag");
 | 
						|
    if (highlightsShadowsOp->shaderProgram)
 | 
						|
    {
 | 
						|
        highlightsShadowsOp->uniforms["highlightsValue"] = {"highlightsValue"};
 | 
						|
        highlightsShadowsOp->uniforms["shadowsValue"] = {"shadowsValue"};
 | 
						|
        highlightsShadowsOp->highlightsVal = &highlights;
 | 
						|
        highlightsShadowsOp->shadowsVal = &shadows;
 | 
						|
        highlightsShadowsOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(highlightsShadowsOp));
 | 
						|
        printf("  + Loaded Highlights/Shadows\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Highlights/Shadows\n");
 | 
						|
 | 
						|
    auto whiteBlackOp = std::make_unique<PipelineOperation>("Whites/Blacks");
 | 
						|
 | 
						|
    whiteBlackOp->shaderProgram =
 | 
						|
        LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert",
 | 
						|
                                   shaderBasePath + "whites_blacks.frag");
 | 
						|
    if (whiteBlackOp->shaderProgram)
 | 
						|
    {
 | 
						|
        whiteBlackOp->uniforms["whitesValue"] = {"whitesValue"};
 | 
						|
        whiteBlackOp->uniforms["blacksValue"] = {"blacksValue"};
 | 
						|
        whiteBlackOp->whitesVal = &whites;
 | 
						|
        whiteBlackOp->blacksVal = &blacks;
 | 
						|
        whiteBlackOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(whiteBlackOp));
 | 
						|
        printf("  + Loaded Whites/Blacks\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Whites/Blacks\n");
 | 
						|
 | 
						|
    auto textureOp = std::make_unique<PipelineOperation>("Texture");
 | 
						|
 | 
						|
    textureOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "texture.frag");
 | 
						|
    if (textureOp->shaderProgram)
 | 
						|
    {
 | 
						|
        textureOp->uniforms["textureValue"] = {"textureValue"};
 | 
						|
        textureOp->textureVal = &texture;
 | 
						|
        textureOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(textureOp));
 | 
						|
        printf("  + Loaded Texture\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Texture\n");
 | 
						|
 | 
						|
    auto clarityOp = std::make_unique<PipelineOperation>("Clarity");
 | 
						|
    clarityOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "clarity.frag");
 | 
						|
    if (clarityOp->shaderProgram)
 | 
						|
    {
 | 
						|
        clarityOp->uniforms["clarityValue"] = {"clarityValue"};
 | 
						|
        clarityOp->clarityVal = &clarity;
 | 
						|
        clarityOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(clarityOp));
 | 
						|
        printf("  + Loaded Clarity\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Clarity\n");
 | 
						|
 | 
						|
    auto dehazeOp = std::make_unique<PipelineOperation>("Dehaze");
 | 
						|
    dehazeOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "dehaze.frag");
 | 
						|
    if (dehazeOp->shaderProgram)
 | 
						|
    {
 | 
						|
        dehazeOp->uniforms["dehazeValue"] = {"dehazeValue"};
 | 
						|
        dehazeOp->dehazeVal = &dehaze;
 | 
						|
        dehazeOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(dehazeOp));
 | 
						|
        printf("  + Loaded Dehaze\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Dehaze\n");
 | 
						|
 | 
						|
    auto saturationOp = std::make_unique<PipelineOperation>("Saturation");
 | 
						|
    saturationOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "saturation.frag");
 | 
						|
    if (saturationOp->shaderProgram)
 | 
						|
    {
 | 
						|
        saturationOp->uniforms["saturationValue"] = {"saturationValue"};
 | 
						|
        saturationOp->saturationVal = &saturation;
 | 
						|
        saturationOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(saturationOp));
 | 
						|
        printf("  + Loaded Saturation\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Saturation\n");
 | 
						|
 | 
						|
    auto vibranceOp = std::make_unique<PipelineOperation>("Vibrance");
 | 
						|
    vibranceOp->shaderProgram = LoadShaderProgramFromFiles(
 | 
						|
        shaderBasePath + "passthrough.vert", shaderBasePath + "vibrance.frag");
 | 
						|
    if (vibranceOp->shaderProgram)
 | 
						|
    {
 | 
						|
        vibranceOp->uniforms["vibranceValue"] = {"vibranceValue"};
 | 
						|
        vibranceOp->vibranceVal = &vibrance;
 | 
						|
        vibranceOp->FindUniformLocations();
 | 
						|
        g_allOperations.push_back(std::move(vibranceOp));
 | 
						|
        printf("  + Loaded Vibrance\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
        printf("  - FAILED Vibrance\n");
 | 
						|
 | 
						|
    g_pipeline.activeOperations.clear();
 | 
						|
    for (const auto &op_ptr : g_allOperations)
 | 
						|
    {
 | 
						|
        if (op_ptr)
 | 
						|
        { // Make sure pointer is valid
 | 
						|
            g_pipeline.activeOperations.push_back(
 | 
						|
                *op_ptr); // Add a *copy* to the active list
 | 
						|
            // Re-find locations for the copy (or ensure copy constructor handles it)
 | 
						|
            g_pipeline.activeOperations.back().FindUniformLocations();
 | 
						|
            // Copy the pointers to the actual slider variables
 | 
						|
            g_pipeline.activeOperations.back().exposureVal = op_ptr->exposureVal;
 | 
						|
            g_pipeline.activeOperations.back().contrastVal = op_ptr->contrastVal;
 | 
						|
            g_pipeline.activeOperations.back().clarityVal = op_ptr->clarityVal;
 | 
						|
            g_pipeline.activeOperations.back().highlightsVal = op_ptr->highlightsVal;
 | 
						|
            g_pipeline.activeOperations.back().shadowsVal = op_ptr->shadowsVal;
 | 
						|
            g_pipeline.activeOperations.back().whitesVal = op_ptr->whitesVal;
 | 
						|
            g_pipeline.activeOperations.back().blacksVal = op_ptr->blacksVal;
 | 
						|
            g_pipeline.activeOperations.back().textureVal = op_ptr->textureVal;
 | 
						|
            g_pipeline.activeOperations.back().dehazeVal = op_ptr->dehazeVal;
 | 
						|
            g_pipeline.activeOperations.back().saturationVal = op_ptr->saturationVal;
 | 
						|
            g_pipeline.activeOperations.back().vibranceVal = op_ptr->vibranceVal;
 | 
						|
            g_pipeline.activeOperations.back().temperatureVal =
 | 
						|
                op_ptr->temperatureVal;
 | 
						|
            g_pipeline.activeOperations.back().tintVal = op_ptr->tintVal;
 | 
						|
 | 
						|
            // Set initial enabled state if needed (e.g., all enabled by default)
 | 
						|
            g_pipeline.activeOperations.back().enabled = true;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    printf("Initialized %zu possible operations. %zu added to default active "
 | 
						|
           "pipeline.\n",
 | 
						|
           g_allOperations.size(), g_pipeline.activeOperations.size());
 | 
						|
}
 | 
						|
 | 
						|
// Add this function somewhere accessible, e.g., before main()
 | 
						|
 | 
						|
void ComputeHistogramGPU(GLuint inputTextureID, int width, int height)
 | 
						|
{
 | 
						|
    if (!g_histogramResourcesInitialized || inputTextureID == 0 || width <= 0 ||
 | 
						|
        height <= 0)
 | 
						|
    {
 | 
						|
        // Clear CPU data if not computed
 | 
						|
        std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
 | 
						|
        g_histogramMaxCount = 1;
 | 
						|
        printf("Histogram resources not initialized or invalid input. Skipping "
 | 
						|
               "computation.\n");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    // 1. Clear the SSBO buffer data to zeros
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
 | 
						|
    // Using glBufferSubData might be marginally faster than glClearBufferData if
 | 
						|
    // driver optimizes zeroing static std::vector<unsigned int>
 | 
						|
    // zeros(HISTOGRAM_BUFFER_SIZE, 0); // Create once
 | 
						|
    // glBufferSubData(GL_SHADER_STORAGE_BUFFER, 0, HISTOGRAM_BUFFER_SIZE *
 | 
						|
    // sizeof(unsigned int), zeros.data()); Or use glClearBufferData (often
 | 
						|
    // recommended)
 | 
						|
    GLuint zero = 0;
 | 
						|
    glClearBufferData(GL_SHADER_STORAGE_BUFFER, GL_R32UI, GL_RED_INTEGER,
 | 
						|
                      GL_UNSIGNED_INT, &zero);
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
 | 
						|
 | 
						|
    // 2. Bind resources and dispatch compute shader
 | 
						|
    glUseProgram(g_histogramComputeShader);
 | 
						|
 | 
						|
    // Bind input texture as image unit 0 (read-only)
 | 
						|
    // IMPORTANT: Ensure the format matches the compute shader layout qualifier
 | 
						|
    // (e.g., rgba8) If textureToDisplay is RGBA16F, you'd use layout(rgba16f) in
 | 
						|
    // shader
 | 
						|
    glBindImageTexture(0, inputTextureID, 0, GL_FALSE, 0, GL_READ_ONLY,
 | 
						|
                       GL_RGBA16); // Assuming display texture is RGBA8
 | 
						|
 | 
						|
    // Bind SSBO to binding point 1
 | 
						|
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, g_histogramSSBO);
 | 
						|
 | 
						|
    // Calculate number of work groups
 | 
						|
    GLuint workGroupSizeX = 16; // Must match layout in shader
 | 
						|
    GLuint workGroupSizeY = 16;
 | 
						|
    GLuint numGroupsX = (width + workGroupSizeX - 1) / workGroupSizeX;
 | 
						|
    GLuint numGroupsY = (height + workGroupSizeY - 1) / workGroupSizeY;
 | 
						|
 | 
						|
    // Dispatch the compute shader
 | 
						|
    glDispatchCompute(numGroupsX, numGroupsY, 1);
 | 
						|
 | 
						|
    // 3. Synchronization: Ensure compute shader writes finish before CPU reads
 | 
						|
    // buffer Use a memory barrier on the SSBO writes
 | 
						|
    glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
 | 
						|
 | 
						|
    // Unbind resources (optional here, but good practice)
 | 
						|
    glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, 0);
 | 
						|
    glBindImageTexture(0, 0, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA16);
 | 
						|
    glUseProgram(0);
 | 
						|
 | 
						|
    // 4. Read histogram data back from SSBO to CPU vector
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, g_histogramSSBO);
 | 
						|
    glGetBufferSubData(GL_SHADER_STORAGE_BUFFER, 0,
 | 
						|
                       HISTOGRAM_BUFFER_SIZE * sizeof(unsigned int),
 | 
						|
                       g_histogramDataCPU.data());
 | 
						|
    glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0); // Unbind
 | 
						|
 | 
						|
    // 5. Find the maximum count for scaling the plot (optional, can be capped)
 | 
						|
    g_histogramMaxCount = 255; // Reset to 255 (prevents div by zero)
 | 
						|
    for (unsigned int count : g_histogramDataCPU)
 | 
						|
    {
 | 
						|
        if (count > g_histogramMaxCount)
 | 
						|
        {
 | 
						|
            g_histogramMaxCount = count;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    // Optional: Cap max count to prevent extreme peaks from flattening the rest
 | 
						|
    // unsigned int capThreshold = (width * height) / 50; // e.g., cap at 2% of
 | 
						|
    // pixels g_histogramMaxCount = std::min(g_histogramMaxCount, capThreshold);
 | 
						|
    // if (g_histogramMaxCount == 0) g_histogramMaxCount = 1; // Ensure not zero
 | 
						|
    // after capping
 | 
						|
 | 
						|
    GLenum err = glGetError();
 | 
						|
    if (err != GL_NO_ERROR)
 | 
						|
    {
 | 
						|
        fprintf(stderr, "OpenGL Error during histogram computation/readback: %u\n",
 | 
						|
                err);
 | 
						|
        // Optionally clear CPU data on error
 | 
						|
        std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
 | 
						|
        g_histogramMaxCount = 1;
 | 
						|
        printf("Histogram computation failed. Data cleared.\n");
 | 
						|
    }
 | 
						|
    else
 | 
						|
    {
 | 
						|
        printf("Histogram computed. Max count: %u\n", g_histogramMaxCount);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
// Add this function somewhere accessible, e.g., before main()
 | 
						|
 | 
						|
void DrawHistogramWidget(const char *widgetId, ImVec2 graphSize)
 | 
						|
{
 | 
						|
    if (g_histogramDataCPU.empty() ||
 | 
						|
        g_histogramMaxCount <= 1)
 | 
						|
    { // Check if data is valid
 | 
						|
        if (g_histogramDataCPU.empty())
 | 
						|
        {
 | 
						|
            ImGui::Text("Histogram data not initialized.");
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            ImGui::Text("Histogram data is empty or invalid.");
 | 
						|
        }
 | 
						|
        if (g_histogramMaxCount <= 1)
 | 
						|
        {
 | 
						|
            ImGui::Text("Histogram max count is invalid.");
 | 
						|
        }
 | 
						|
        ImGui::Text("Histogram data not available.");
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    ImGui::PushID(widgetId); // Isolate widget IDs
 | 
						|
 | 
						|
    ImDrawList *drawList = ImGui::GetWindowDrawList();
 | 
						|
    const ImVec2 widgetPos = ImGui::GetCursorScreenPos();
 | 
						|
 | 
						|
    // Determine actual graph size (negative values mean use available space)
 | 
						|
    if (graphSize.x <= 0.0f)
 | 
						|
        graphSize.x = ImGui::GetContentRegionAvail().x;
 | 
						|
    if (graphSize.y <= 0.0f)
 | 
						|
        graphSize.y = 100.0f; // Default height
 | 
						|
 | 
						|
    // Draw background for the histogram area (optional)
 | 
						|
    drawList->AddRectFilled(widgetPos, widgetPos + graphSize,
 | 
						|
                            IM_COL32(30, 30, 30, 200));
 | 
						|
 | 
						|
    // Calculate scaling factors
 | 
						|
    float barWidth = graphSize.x / float(NUM_HISTOGRAM_BINS);
 | 
						|
    float scaleY =
 | 
						|
        graphSize.y / float(g_histogramMaxCount); // Scale based on max count
 | 
						|
 | 
						|
    // Define colors (with some transparency for overlap visibility)
 | 
						|
    const ImU32 colR = IM_COL32(255, 0, 0, 180);
 | 
						|
    const ImU32 colG = IM_COL32(0, 255, 0, 180);
 | 
						|
    const ImU32 colB = IM_COL32(0, 0, 255, 180);
 | 
						|
 | 
						|
    // Draw the histogram bars (R, G, B)
 | 
						|
    for (int i = 0; i < NUM_HISTOGRAM_BINS; ++i)
 | 
						|
    {
 | 
						|
        // Get heights (clamped to graph size)
 | 
						|
        float hR = ImMin(float(g_histogramDataCPU[i]) * scaleY, graphSize.y);
 | 
						|
        float hG = ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS]) * scaleY,
 | 
						|
                         graphSize.y);
 | 
						|
        float hB =
 | 
						|
            ImMin(float(g_histogramDataCPU[i + NUM_HISTOGRAM_BINS * 2]) * scaleY,
 | 
						|
                  graphSize.y);
 | 
						|
 | 
						|
        // Calculate bar positions
 | 
						|
        float x0 = widgetPos.x + float(i) * barWidth;
 | 
						|
        float x1 = x0 + barWidth;                // Use lines if bars are too thin, or thin rects
 | 
						|
        float yBase = widgetPos.y + graphSize.y; // Bottom of the graph
 | 
						|
 | 
						|
        // Draw lines or thin rectangles (lines are often better for dense
 | 
						|
        // histograms) Overlap/Blend: Draw B, then G, then R so Red is most
 | 
						|
        // prominent? Or use alpha blending.
 | 
						|
        if (hB > 0)
 | 
						|
            drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase),
 | 
						|
                              ImVec2(x0 + barWidth * 0.5f, yBase - hB), colB, 1.0f);
 | 
						|
        if (hG > 0)
 | 
						|
            drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase),
 | 
						|
                              ImVec2(x0 + barWidth * 0.5f, yBase - hG), colG, 1.0f);
 | 
						|
        if (hR > 0)
 | 
						|
            drawList->AddLine(ImVec2(x0 + barWidth * 0.5f, yBase),
 | 
						|
                              ImVec2(x0 + barWidth * 0.5f, yBase - hR), colR, 1.0f);
 | 
						|
 | 
						|
        // --- Alternative: Rectangles (might overlap heavily) ---
 | 
						|
        // if (hB > 0) drawList->AddRectFilled(ImVec2(x0, yBase - hB), ImVec2(x1,
 | 
						|
        // yBase), colB); if (hG > 0) drawList->AddRectFilled(ImVec2(x0, yBase -
 | 
						|
        // hG), ImVec2(x1, yBase), colG); if (hR > 0)
 | 
						|
        // drawList->AddRectFilled(ImVec2(x0, yBase - hR), ImVec2(x1, yBase), colR);
 | 
						|
    }
 | 
						|
 | 
						|
    // Draw border around the histogram area (optional)
 | 
						|
    drawList->AddRect(widgetPos, widgetPos + graphSize,
 | 
						|
                      IM_COL32(150, 150, 150, 255));
 | 
						|
 | 
						|
    // Advance cursor past the histogram widget area
 | 
						|
    ImGui::Dummy(graphSize);
 | 
						|
 | 
						|
    ImGui::PopID(); // Restore ID stack
 | 
						|
}
 | 
						|
 | 
						|
// Main code
 | 
						|
int main(int, char **)
 | 
						|
{
 | 
						|
    // Setup SDL
 | 
						|
    if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) !=
 | 
						|
        0)
 | 
						|
    {
 | 
						|
        printf("Error: %s\n", SDL_GetError());
 | 
						|
        return -1;
 | 
						|
    }
 | 
						|
 | 
						|
    // Decide GL+GLSL versions
 | 
						|
#if defined(IMGUI_IMPL_OPENGL_ES2)
 | 
						|
    // GL ES 2.0 + GLSL 100 (WebGL 1.0)
 | 
						|
    const char *glsl_version = "#version 100";
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
 | 
						|
#elif defined(IMGUI_IMPL_OPENGL_ES3)
 | 
						|
    // GL ES 3.0 + GLSL 300 es (WebGL 2.0)
 | 
						|
    const char *glsl_version = "#version 300 es";
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
 | 
						|
#elif defined(__APPLE__)
 | 
						|
    // GL 3.2 Core + GLSL 150
 | 
						|
    const char *glsl_version = "#version 150";
 | 
						|
    SDL_GL_SetAttribute(
 | 
						|
        SDL_GL_CONTEXT_FLAGS,
 | 
						|
        SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG); // Always required on Mac
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
 | 
						|
#else
 | 
						|
    // GL 3.0 + GLSL 130
 | 
						|
    const char *glsl_version = "#version 130";
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
 | 
						|
#endif
 | 
						|
 | 
						|
    // From 2.0.18: Enable native IME.
 | 
						|
#ifdef SDL_HINT_IME_SHOW_UI
 | 
						|
    SDL_SetHint(SDL_HINT_IME_SHOW_UI, "1");
 | 
						|
#endif
 | 
						|
 | 
						|
    // Create window with graphics context
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
 | 
						|
    SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
 | 
						|
    SDL_WindowFlags window_flags =
 | 
						|
        (SDL_WindowFlags)(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE |
 | 
						|
                          SDL_WINDOW_ALLOW_HIGHDPI);
 | 
						|
    SDL_Window *window =
 | 
						|
        SDL_CreateWindow("tedit", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
 | 
						|
                         1280, 720, window_flags);
 | 
						|
    if (window == nullptr)
 | 
						|
    {
 | 
						|
        printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError());
 | 
						|
        return -1;
 | 
						|
    }
 | 
						|
 | 
						|
    SDL_GLContext gl_context = SDL_GL_CreateContext(window);
 | 
						|
    if (gl_context == nullptr)
 | 
						|
    {
 | 
						|
        printf("Error: SDL_GL_CreateContext(): %s\n", SDL_GetError());
 | 
						|
        return -1;
 | 
						|
    }
 | 
						|
 | 
						|
    SDL_GL_MakeCurrent(window, gl_context);
 | 
						|
    SDL_GL_SetSwapInterval(1); // Enable vsync
 | 
						|
 | 
						|
    glewExperimental = GL_TRUE; // Needed for core profile
 | 
						|
    GLenum err = glewInit();
 | 
						|
    if (err != GLEW_OK)
 | 
						|
    {
 | 
						|
        fprintf(stderr, "Error: %s\n", glewGetErrorString(err));
 | 
						|
        return -1;
 | 
						|
    }
 | 
						|
 | 
						|
    // Setup Dear ImGui context
 | 
						|
    IMGUI_CHECKVERSION();
 | 
						|
    ImGui::CreateContext();
 | 
						|
    ImGuiIO &io = ImGui::GetIO();
 | 
						|
    (void)io;
 | 
						|
    io.ConfigFlags |=
 | 
						|
        ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
 | 
						|
    io.ConfigFlags |=
 | 
						|
        ImGuiConfigFlags_NavEnableGamepad;            // Enable Gamepad Controls
 | 
						|
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable; // Enable Docking
 | 
						|
    // io.ConfigFlags |= ImGuiConfigFlags_ViewportsEnable;       // Enable
 | 
						|
    // Multi-Viewport / Platform Windows io.ConfigViewportsNoAutoMerge = true;
 | 
						|
    // io.ConfigViewportsNoTaskBarIcon = true;
 | 
						|
 | 
						|
    // Setup Dear ImGui style
 | 
						|
    ImGui::StyleColorsDark();
 | 
						|
    // ImGui::StyleColorsLight();
 | 
						|
 | 
						|
    // When viewports are enabled we tweak WindowRounding/WindowBg so platform
 | 
						|
    // windows can look identical to regular ones.
 | 
						|
    ImGuiStyle &style = ImGui::GetStyle();
 | 
						|
    if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
 | 
						|
    {
 | 
						|
        style.WindowRounding = 0.0f;
 | 
						|
        style.Colors[ImGuiCol_WindowBg].w = 1.0f;
 | 
						|
    }
 | 
						|
 | 
						|
    // Setup Platform/Renderer backends
 | 
						|
    ImGui_ImplSDL2_InitForOpenGL(window, gl_context);
 | 
						|
    ImGui_ImplOpenGL3_Init(glsl_version);
 | 
						|
 | 
						|
    // Our state
 | 
						|
    ImVec4 clear_color = ImVec4(0.45f, 0.55f, 0.60f, 1.00f);
 | 
						|
 | 
						|
    g_openFileDialog.SetTitle("Open Image File");
 | 
						|
    // Add common image formats and typical RAW formats
 | 
						|
    g_openFileDialog.SetTypeFilters({
 | 
						|
        ".jpg", ".jpeg", ".png", ".tif", ".tiff", // Standard formats
 | 
						|
        ".arw", ".cr2", ".cr3", ".nef", ".dng", ".orf", ".raf",
 | 
						|
        ".rw2", // Common RAW
 | 
						|
        ".*"    // Allow any file as fallback
 | 
						|
    });
 | 
						|
 | 
						|
    g_exportSaveFileDialog.SetTitle("Export Image As");
 | 
						|
    // Type filters for saving are less critical as we force the extension later,
 | 
						|
    // but can be helpful for user navigation. Let's set a default.
 | 
						|
    g_exportSaveFileDialog.SetTypeFilters({".jpg", ".png", ".tif"});
 | 
						|
 | 
						|
    AppImage g_loadedImage; // Your loaded image data
 | 
						|
    bool g_imageIsLoaded = false;
 | 
						|
    g_processedTextureId = 0; // Initialize processed texture ID
 | 
						|
    printf("Initializing image processing pipeline...\n");
 | 
						|
    g_pipeline.Init("shaders/"); // Assuming shaders are in shaders/ subdir
 | 
						|
 | 
						|
    ImGuiTexInspect::ImplOpenGL3_Init(); // Or DirectX 11 equivalent (check your
 | 
						|
                                         // chosen backend header file)
 | 
						|
    ImGuiTexInspect::Init();
 | 
						|
    ImGuiTexInspect::CreateContext();
 | 
						|
 | 
						|
    InitShaderOperations("shaders/"); // Initialize shader operations
 | 
						|
 | 
						|
    if (!InitHistogramResources("shaders/"))
 | 
						|
    {
 | 
						|
        // Handle error - maybe disable histogram feature
 | 
						|
        fprintf(stderr, "Histogram initialization failed, feature disabled.\n");
 | 
						|
    }
 | 
						|
 | 
						|
    // Main loop
 | 
						|
    bool done = false;
 | 
						|
#ifdef __EMSCRIPTEN__
 | 
						|
    // For an Emscripten build we are disabling file-system access, so let's not
 | 
						|
    // attempt to do a fopen() of the imgui.ini file. You may manually call
 | 
						|
    // LoadIniSettingsFromMemory() to load settings from your own storage.
 | 
						|
    io.IniFilename = nullptr;
 | 
						|
    EMSCRIPTEN_MAINLOOP_BEGIN
 | 
						|
#else
 | 
						|
    while (!done)
 | 
						|
#endif
 | 
						|
    {
 | 
						|
        // Poll and handle events (inputs, window resize, etc.)
 | 
						|
        // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to
 | 
						|
        // tell if dear imgui wants to use your inputs.
 | 
						|
        // - When io.WantCaptureMouse is true, do not dispatch mouse input data to
 | 
						|
        // your main application, or clear/overwrite your copy of the mouse data.
 | 
						|
        // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input
 | 
						|
        // data to your main application, or clear/overwrite your copy of the
 | 
						|
        // keyboard data. Generally you may always pass all inputs to dear imgui,
 | 
						|
        // and hide them from your application based on those two flags.
 | 
						|
        SDL_Event event;
 | 
						|
        while (SDL_PollEvent(&event))
 | 
						|
        {
 | 
						|
            ImGui_ImplSDL2_ProcessEvent(&event);
 | 
						|
            if (event.type == SDL_QUIT)
 | 
						|
                done = true;
 | 
						|
            if (event.type == SDL_WINDOWEVENT &&
 | 
						|
                event.window.event == SDL_WINDOWEVENT_CLOSE &&
 | 
						|
                event.window.windowID == SDL_GetWindowID(window))
 | 
						|
                done = true;
 | 
						|
        }
 | 
						|
        if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED)
 | 
						|
        {
 | 
						|
            SDL_Delay(10);
 | 
						|
            continue;
 | 
						|
        }
 | 
						|
 | 
						|
        // Start the Dear ImGui frame
 | 
						|
        ImGui_ImplOpenGL3_NewFrame();
 | 
						|
        ImGui_ImplSDL2_NewFrame();
 | 
						|
        ImGui::NewFrame();
 | 
						|
 | 
						|
        GLuint textureToDisplay = 0; // Use a local var for clarity
 | 
						|
        GLuint textureToSave = 0;    // Texture ID holding final linear data for saving
 | 
						|
        if (g_imageIsLoaded && g_loadedImage.m_textureId != 0)
 | 
						|
        {
 | 
						|
            // Update pipeline's knowledge of color spaces (might trigger MarkDirty inside if they change)
 | 
						|
            // Ideally, check if they *actually* changed before marking dirty.
 | 
						|
            if (g_pipeline.inputColorSpace != g_inputColorSpace)
 | 
						|
            {
 | 
						|
                g_pipeline.inputColorSpace = g_inputColorSpace;
 | 
						|
                g_pipeline.MarkDirty();
 | 
						|
            }
 | 
						|
            if (g_pipeline.outputColorSpace != g_outputColorSpace)
 | 
						|
            {
 | 
						|
                g_pipeline.outputColorSpace = g_outputColorSpace;
 | 
						|
                g_pipeline.MarkDirty();
 | 
						|
            }
 | 
						|
            bool recomputeHisto = false;
 | 
						|
 | 
						|
            if (g_pipeline.IsDirty())   
 | 
						|
            {
 | 
						|
                recomputeHisto = true;
 | 
						|
            }
 | 
						|
 | 
						|
            // Get potentially cached textures
 | 
						|
            textureToDisplay = g_pipeline.GetProcessedTexture(
 | 
						|
                g_loadedImage.m_textureId,
 | 
						|
                g_loadedImage.getWidth(),
 | 
						|
                g_loadedImage.getHeight(),
 | 
						|
                true // Request texture suitable for display (potentially sRGB)
 | 
						|
            );
 | 
						|
 | 
						|
            textureToSave = g_pipeline.GetProcessedTexture(
 | 
						|
                g_loadedImage.m_textureId,
 | 
						|
                g_loadedImage.getWidth(),
 | 
						|
                g_loadedImage.getHeight(),
 | 
						|
                false // Request texture suitable for saving (linear)
 | 
						|
            );
 | 
						|
 | 
						|
            // Update histogram only if the display texture is valid
 | 
						|
            if (recomputeHisto && textureToDisplay != 0)
 | 
						|
            {
 | 
						|
                // Optional optimization: Only compute histogram if pipeline was dirty this frame?
 | 
						|
                // Or maybe compute less frequently? For now, compute if texture is valid.
 | 
						|
                ComputeHistogramGPU(textureToDisplay, g_loadedImage.getWidth(), g_loadedImage.getHeight());
 | 
						|
            }
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            textureToDisplay = 0;
 | 
						|
            textureToSave = 0;
 | 
						|
            // Ensure pipeline cache is cleared if no image is loaded
 | 
						|
            g_pipeline.GetProcessedTexture(0, 0, 0, true); // Call with invalid input clears cache
 | 
						|
                                                           // Clear histogram data if no image loaded
 | 
						|
            std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
 | 
						|
            g_histogramMaxCount = 1;
 | 
						|
        }
 | 
						|
 | 
						|
        // --- Menu Bar ---
 | 
						|
        if (ImGui::BeginMainMenuBar())
 | 
						|
        {
 | 
						|
            if (ImGui::BeginMenu("File"))
 | 
						|
            {
 | 
						|
                if (ImGui::MenuItem("Open...", "Ctrl+O"))
 | 
						|
                {
 | 
						|
                    g_openFileDialog.Open();
 | 
						|
                }
 | 
						|
                // Disable Export if no image is loaded
 | 
						|
                if (ImGui::MenuItem("Export...", "Ctrl+E", false, g_imageIsLoaded))
 | 
						|
                {
 | 
						|
                    g_exportErrorMsg = "";     // Clear previous errors
 | 
						|
                    g_showExportWindow = true; // <<< Set the flag to show the window
 | 
						|
                }
 | 
						|
                ImGui::Separator();
 | 
						|
                if (ImGui::MenuItem("Exit"))
 | 
						|
                {
 | 
						|
                    done = true; // Simple exit for now
 | 
						|
                }
 | 
						|
                ImGui::EndMenu();
 | 
						|
            }
 | 
						|
            // ... other menus ...
 | 
						|
            ImGui::EndMainMenuBar();
 | 
						|
        }
 | 
						|
 | 
						|
        // --- File Dialog Display & Handling ---
 | 
						|
        g_openFileDialog.Display();
 | 
						|
        g_exportSaveFileDialog.Display();
 | 
						|
 | 
						|
        if (g_openFileDialog.HasSelected())
 | 
						|
        {
 | 
						|
            std::string selectedPath = g_openFileDialog.GetSelected().string();
 | 
						|
            g_openFileDialog.ClearSelected();
 | 
						|
            printf("Opening file: %s\n", selectedPath.c_str());
 | 
						|
 | 
						|
            // --- Load the selected image ---
 | 
						|
            std::optional<AppImage> imgOpt = loadImage(selectedPath);
 | 
						|
            if (imgOpt)
 | 
						|
            {
 | 
						|
                // If an image was already loaded, clean up its texture first
 | 
						|
                if (g_loadedImage.m_textureId != 0)
 | 
						|
                {
 | 
						|
                    glDeleteTextures(1, &g_loadedImage.m_textureId);
 | 
						|
                    g_loadedImage.m_textureId = 0;
 | 
						|
                }
 | 
						|
                // Clean up pipeline resources (FBOs/Textures) before loading new
 | 
						|
                // texture
 | 
						|
                g_pipeline.ResetResources(); // <<< NEED TO ADD THIS METHOD
 | 
						|
 | 
						|
                g_loadedImage = std::move(*imgOpt);
 | 
						|
                printf("Image loaded (%dx%d, %d channels, Linear:%s)\n",
 | 
						|
                       g_loadedImage.getWidth(), g_loadedImage.getHeight(),
 | 
						|
                       g_loadedImage.getChannels(),
 | 
						|
                       g_loadedImage.isLinear() ? "Yes" : "No");
 | 
						|
 | 
						|
                if (loadImageTexture(g_loadedImage))
 | 
						|
                {
 | 
						|
                    g_imageIsLoaded = true;
 | 
						|
                    g_currentFilePath = selectedPath; // Store path
 | 
						|
                    printf("Float texture created successfully (ID: %u).\n",
 | 
						|
                           g_loadedImage.m_textureId);
 | 
						|
                    // Maybe reset sliders/pipeline state? Optional.
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    g_imageIsLoaded = false;
 | 
						|
                    g_currentFilePath = "";
 | 
						|
                    fprintf(stderr, "Failed to load image into GL texture.\n");
 | 
						|
                    // TODO: Show error to user (e.g., modal popup)
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                g_imageIsLoaded = false;
 | 
						|
                g_currentFilePath = "";
 | 
						|
                fprintf(stderr, "Failed to load image file: %s\n",
 | 
						|
                        selectedPath.c_str());
 | 
						|
                // TODO: Show error to user
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (g_showExportWindow) // <<< Only attempt to draw if flag is true
 | 
						|
        {
 | 
						|
            // Optional: Center the window the first time it appears
 | 
						|
            ImGui::SetNextWindowSize(ImVec2(400, 0),
 | 
						|
                                     ImGuiCond_Appearing); // Auto-height
 | 
						|
            ImVec2 center = ImGui::GetMainViewport()->GetCenter();
 | 
						|
            ImGui::SetNextWindowPos(center, ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
 | 
						|
 | 
						|
            // Begin a standard window. Pass &g_showExportWindow to enable the 'X'
 | 
						|
            // button.
 | 
						|
            if (ImGui::Begin("Export Settings", &g_showExportWindow,
 | 
						|
                             ImGuiWindowFlags_AlwaysAutoResize))
 | 
						|
            {
 | 
						|
                ImGui::Text("Choose Export Format and Settings:");
 | 
						|
                ImGui::Separator();
 | 
						|
 | 
						|
                // --- Format Selection ---
 | 
						|
                ImGui::Text("Format:");
 | 
						|
                ImGui::SameLine();
 | 
						|
                // ... (Combo box logic for g_exportFormat remains the same) ...
 | 
						|
                const char *formats[] = {"JPEG", "PNG (8-bit)", "PNG (16-bit)",
 | 
						|
                                         "TIFF (8-bit)", "TIFF (16-bit)"};
 | 
						|
                int currentFormatIndex = 0;
 | 
						|
                switch (g_exportFormat)
 | 
						|
                { /* ... map g_exportFormat to index ... */
 | 
						|
                }
 | 
						|
                if (ImGui::Combo("##ExportFormat", ¤tFormatIndex, formats,
 | 
						|
                                 IM_ARRAYSIZE(formats)))
 | 
						|
                {
 | 
						|
                    switch (currentFormatIndex)
 | 
						|
                    { /* ... map index back to g_exportFormat
 | 
						|
                         ... */
 | 
						|
                    }
 | 
						|
                    g_exportErrorMsg = "";
 | 
						|
                }
 | 
						|
 | 
						|
                // --- Format Specific Options ---
 | 
						|
                if (g_exportFormat == ImageSaveFormat::JPEG)
 | 
						|
                {
 | 
						|
                    ImGui::SliderInt("Quality", &g_exportQuality, 1, 100);
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    ImGui::Dummy(ImVec2(
 | 
						|
                        0.0f,
 | 
						|
                        ImGui::GetFrameHeightWithSpacing())); // Keep consistent height
 | 
						|
                }
 | 
						|
                ImGui::Separator();
 | 
						|
 | 
						|
                // --- Display Error Messages ---
 | 
						|
                if (!g_exportErrorMsg.empty())
 | 
						|
                {
 | 
						|
                    ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.2f, 0.2f, 1.0f));
 | 
						|
                    ImGui::TextWrapped("Error: %s", g_exportErrorMsg.c_str());
 | 
						|
                    ImGui::PopStyleColor();
 | 
						|
                    ImGui::Separator();
 | 
						|
                }
 | 
						|
 | 
						|
                // --- Action Buttons ---
 | 
						|
                if (ImGui::Button("Save As...", ImVec2(120, 0)))
 | 
						|
                {
 | 
						|
                    // ... (Logic to set default name/path and call
 | 
						|
                    // g_exportSaveFileDialog.Open() remains the same) ...
 | 
						|
                    std::filesystem::path currentPath(g_currentFilePath);
 | 
						|
                    std::string defaultName = currentPath.stem().string() + "_edited";
 | 
						|
                    g_exportSaveFileDialog.SetPwd(currentPath.parent_path());
 | 
						|
                    // g_exportSaveFileDialog.SetInputName(defaultName); // If supported
 | 
						|
                    g_exportSaveFileDialog.Open();
 | 
						|
                }
 | 
						|
                ImGui::SameLine();
 | 
						|
                // No need for an explicit Cancel button if the 'X' works, but can keep
 | 
						|
                // it:
 | 
						|
                if (ImGui::Button("Cancel", ImVec2(120, 0)))
 | 
						|
                {
 | 
						|
                    g_showExportWindow = false; // Close the window by setting the flag
 | 
						|
                }
 | 
						|
 | 
						|
            } // Matches ImGui::Begin("Export Settings",...)
 | 
						|
            ImGui::End(); // IMPORTANT: Always call End() for Begin()
 | 
						|
 | 
						|
        } // End of if(g_showExportWindow)
 | 
						|
 | 
						|
        // --- Handle Export Save Dialog Selection ---
 | 
						|
        if (g_exportSaveFileDialog.HasSelected())
 | 
						|
        {
 | 
						|
            // ... (Your existing logic to get path, correct extension) ...
 | 
						|
            std::filesystem::path savePathFs = g_exportSaveFileDialog.GetSelected();
 | 
						|
            g_exportSaveFileDialog.ClearSelected();
 | 
						|
            std::string savePath = savePathFs.string();
 | 
						|
            // ... (Ensure/correct extension logic) ...
 | 
						|
 | 
						|
            // --- Get Processed Image Data & Save ---
 | 
						|
            printf("Attempting to save to: %s\n", savePath.c_str());
 | 
						|
            g_exportErrorMsg = "";
 | 
						|
 | 
						|
            if (textureToSave != 0)
 | 
						|
            {
 | 
						|
                AppImage exportImageRGBA; // Name it clearly - it holds RGBA data
 | 
						|
                printf("Reading back texture ID %u for saving...\n", textureToSave);
 | 
						|
                if (ReadTextureToAppImage(textureToSave, g_loadedImage.getWidth(),
 | 
						|
                                          g_loadedImage.getHeight(), exportImageRGBA))
 | 
						|
                {
 | 
						|
                    printf("Texture readback successful, saving...\n");
 | 
						|
                    // <<< --- ADD CONVERSION LOGIC HERE --- >>>
 | 
						|
                    bool saveResult = false;
 | 
						|
                    if (g_exportFormat == ImageSaveFormat::JPEG)
 | 
						|
                    {
 | 
						|
                        // JPEG cannot handle 4 channels, convert to 3 (RGB)
 | 
						|
                        if (exportImageRGBA.getChannels() == 4)
 | 
						|
                        {
 | 
						|
                            printf("JPEG selected: Converting 4-channel RGBA to 3-channel "
 | 
						|
                                   "RGB...\n");
 | 
						|
                            AppImage exportImageRGB(exportImageRGBA.getWidth(),
 | 
						|
                                                    exportImageRGBA.getHeight(), 3);
 | 
						|
                            // Check allocation success? (Should be fine if RGBA worked)
 | 
						|
 | 
						|
                            const float *rgbaData = exportImageRGBA.getData();
 | 
						|
                            float *rgbData = exportImageRGB.getData();
 | 
						|
                            size_t numPixels =
 | 
						|
                                exportImageRGBA.getWidth() * exportImageRGBA.getHeight();
 | 
						|
 | 
						|
                            for (size_t i = 0; i < numPixels; ++i)
 | 
						|
                            {
 | 
						|
                                // Copy R, G, B; discard A
 | 
						|
                                rgbData[i * 3 + 0] = rgbaData[i * 4 + 0]; // R
 | 
						|
                                rgbData[i * 3 + 1] = rgbaData[i * 4 + 1]; // G
 | 
						|
                                rgbData[i * 3 + 2] = rgbaData[i * 4 + 2]; // B
 | 
						|
                            }
 | 
						|
                            exportImageRGB.m_isLinear =
 | 
						|
                                exportImageRGBA.isLinear(); // Preserve linearity flag
 | 
						|
                            exportImageRGB.m_colorSpaceName =
 | 
						|
                                exportImageRGBA
 | 
						|
                                    .getColorSpaceName(); // Preserve colorspace info
 | 
						|
 | 
						|
                            printf("Conversion complete, saving RGB data...\n");
 | 
						|
                            saveResult = saveImage(exportImageRGB, savePath, g_exportFormat,
 | 
						|
                                                   g_exportQuality);
 | 
						|
                        }
 | 
						|
                        else
 | 
						|
                        {
 | 
						|
                            // Source wasn't 4 channels? Unexpected, but save it directly.
 | 
						|
                            printf("Warning: Expected 4 channels for JPEG conversion, got "
 | 
						|
                                   "%d. Saving directly...\n",
 | 
						|
                                   exportImageRGBA.getChannels());
 | 
						|
                            saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat,
 | 
						|
                                                   g_exportQuality);
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        // Format is PNG or TIFF, which should handle 4 channels (or 1/3)
 | 
						|
                        printf("Saving image with original channels (%d) for PNG/TIFF...\n",
 | 
						|
                               exportImageRGBA.getChannels());
 | 
						|
                        saveResult = saveImage(exportImageRGBA, savePath, g_exportFormat,
 | 
						|
                                               g_exportQuality);
 | 
						|
                    }
 | 
						|
                    // <<< --- END CONVERSION LOGIC --- >>>
 | 
						|
                    if (saveResult)
 | 
						|
                    {
 | 
						|
                        printf("Image saved successfully!\n");
 | 
						|
                        g_showExportWindow =
 | 
						|
                            false; // <<< Close the settings window on success
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        fprintf(stderr, "Failed to save image.\n");
 | 
						|
                        g_exportErrorMsg = "Failed to save image data to file.";
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    fprintf(stderr, "Failed to read back texture data from GPU.\n");
 | 
						|
                    g_exportErrorMsg = "Failed to read processed image data from GPU.";
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                fprintf(stderr, "Cannot save: Invalid processed texture ID.\n");
 | 
						|
                g_exportErrorMsg = "No valid processed image data available to save.";
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        static bool use_dockspace = true;
 | 
						|
        if (use_dockspace)
 | 
						|
        {
 | 
						|
            ImGuiViewport *viewport = ImGui::GetMainViewport();
 | 
						|
            ImGuiID dockspace_id = ImGui::GetID("MyDockSpace");
 | 
						|
 | 
						|
            // Use DockSpaceOverViewport instead of creating a manual window
 | 
						|
            // Set the viewport size for the dockspace node. This is important.
 | 
						|
            ImGui::SetNextWindowPos(viewport->WorkPos);
 | 
						|
            ImGui::SetNextWindowSize(viewport->WorkSize);
 | 
						|
            ImGui::SetNextWindowViewport(viewport->ID);
 | 
						|
 | 
						|
            // Use PassthruCentralNode to make the central node background transparent
 | 
						|
            // so the ImGui default background shows until a window is docked there.
 | 
						|
            ImGuiDockNodeFlags dockspace_flags =
 | 
						|
                ImGuiDockNodeFlags_PassthruCentralNode;
 | 
						|
 | 
						|
            // We wrap the DockSpace call in a window that doesn't really draw
 | 
						|
            // anything itself, but is required by the DockBuilder mechanism to target
 | 
						|
            // the space. Make it borderless, no title, etc.
 | 
						|
            ImGuiWindowFlags host_window_flags = 0;
 | 
						|
            host_window_flags |= ImGuiWindowFlags_NoTitleBar |
 | 
						|
                                 ImGuiWindowFlags_NoCollapse |
 | 
						|
                                 ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove;
 | 
						|
            host_window_flags |=
 | 
						|
                ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus;
 | 
						|
            host_window_flags |=
 | 
						|
                ImGuiWindowFlags_NoBackground; // Make the host window transparent
 | 
						|
 | 
						|
            ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
 | 
						|
            ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
 | 
						|
            ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
 | 
						|
            ImGui::Begin("DockSpaceWindowHost", nullptr,
 | 
						|
                         host_window_flags); // No bool* needed
 | 
						|
            ImGui::PopStyleVar(3);
 | 
						|
 | 
						|
            // Create the actual dockspace area.
 | 
						|
            ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags);
 | 
						|
 | 
						|
            ImGui::End(); // End the transparent host window
 | 
						|
 | 
						|
            // --- DockBuilder setup (runs once) ---
 | 
						|
            // This logic remains the same, targeting the dockspace_id
 | 
						|
            // Use DockBuilderGetNode()->IsEmpty() as a robust check for first time
 | 
						|
            // setup or reset.
 | 
						|
            ImGuiDockNode *centralNode = ImGui::DockBuilderGetNode(dockspace_id);
 | 
						|
            if (centralNode == nullptr || centralNode->IsEmpty())
 | 
						|
            {
 | 
						|
                printf("DockBuilder: Setting up initial layout for DockID %u\n",
 | 
						|
                       dockspace_id);
 | 
						|
                ImGui::DockBuilderRemoveNode(
 | 
						|
                    dockspace_id); // Clear out any previous state
 | 
						|
                ImGui::DockBuilderAddNode(dockspace_id, ImGuiDockNodeFlags_DockSpace);
 | 
						|
                ImGui::DockBuilderSetNodeSize(
 | 
						|
                    dockspace_id, viewport->Size); // Set the size for the root node
 | 
						|
 | 
						|
                ImGuiID dock_main_id =
 | 
						|
                    dockspace_id; // This is the ID of the node just added
 | 
						|
                ImGuiID dock_right_id, dock_left_id, dock_center_id;
 | 
						|
 | 
						|
                // Split right first (Edit Panel)
 | 
						|
                ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.25f,
 | 
						|
                                            &dock_right_id, &dock_main_id);
 | 
						|
                // Then split left from the remaining main area (Exif Panel)
 | 
						|
                ImGui::DockBuilderSplitNode(
 | 
						|
                    dock_main_id, ImGuiDir_Left, 0.25f, &dock_left_id,
 | 
						|
                    &dock_center_id); // dock_center_id is the final remaining central
 | 
						|
                                      // node
 | 
						|
 | 
						|
                // Dock the windows into the nodes
 | 
						|
                ImGui::DockBuilderDockWindow("Image Exif", dock_left_id);
 | 
						|
                ImGui::DockBuilderDockWindow("Edit Image", dock_right_id);
 | 
						|
                ImGui::DockBuilderDockWindow(
 | 
						|
                    "Image View", dock_center_id); // Dock image view into the center
 | 
						|
 | 
						|
                ImGui::DockBuilderFinish(dockspace_id);
 | 
						|
                printf("DockBuilder: Layout finished.\n");
 | 
						|
            }
 | 
						|
            // --- End DockBuilder setup ---
 | 
						|
 | 
						|
            // --- Now Begin the actual windows that get docked ---
 | 
						|
            // These calls are now *outside* any manual container window.
 | 
						|
            // They will find their place in the dockspace based on the DockBuilder
 | 
						|
            // setup or user interaction.
 | 
						|
 | 
						|
            // "Image View" window
 | 
						|
            ImGui::Begin("Image View");
 | 
						|
            // Display the texture that HAS the output conversion applied
 | 
						|
            ImVec2 imageWidgetTopLeftScreen =
 | 
						|
                ImGui::GetCursorScreenPos(); // Position BEFORE the inspector panel
 | 
						|
            ImVec2 availableContentSize =
 | 
						|
                ImGui::GetContentRegionAvail(); // Size available FOR the inspector
 | 
						|
                                                // panel
 | 
						|
 | 
						|
            GLuint displayTexId = textureToDisplay; // Use the display texture ID
 | 
						|
            if (displayTexId != 0)
 | 
						|
            {
 | 
						|
                // Assume ImGuiTexInspect fills available space. This might need
 | 
						|
                // adjustment.
 | 
						|
                ImVec2 displaySize = availableContentSize;
 | 
						|
                float displayAspect = displaySize.x / displaySize.y;
 | 
						|
                float imageAspect =
 | 
						|
                    float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight());
 | 
						|
 | 
						|
                ImVec2 imageDisplaySize; // Actual size the image occupies on screen
 | 
						|
                                         // (letterboxed/pillarboxed)
 | 
						|
                ImVec2 imageDisplayOffset =
 | 
						|
                    ImVec2(0, 0); // Offset within the widget area due to letterboxing
 | 
						|
 | 
						|
                if (displayAspect > imageAspect)
 | 
						|
                { // Display is wider than image ->
 | 
						|
                  // letterbox (bars top/bottom)
 | 
						|
                    imageDisplaySize.y = displaySize.y;
 | 
						|
                    imageDisplaySize.x = imageDisplaySize.y * imageAspect;
 | 
						|
                    imageDisplayOffset.x = (displaySize.x - imageDisplaySize.x) * 0.5f;
 | 
						|
                }
 | 
						|
                else
 | 
						|
                { // Display is taller than image (or same aspect) -> pillarbox
 | 
						|
                  // (bars left/right)
 | 
						|
                    imageDisplaySize.x = displaySize.x;
 | 
						|
                    imageDisplaySize.y = imageDisplaySize.x / imageAspect;
 | 
						|
                    imageDisplayOffset.y = (displaySize.y - imageDisplaySize.y) * 0.5f;
 | 
						|
                }
 | 
						|
 | 
						|
                ImVec2 imageTopLeftScreen =
 | 
						|
                    imageWidgetTopLeftScreen + imageDisplayOffset;
 | 
						|
                ImVec2 imageBottomRightScreen = imageTopLeftScreen + imageDisplaySize;
 | 
						|
                // Use textureToDisplay here
 | 
						|
                ImGuiTexInspect::BeginInspectorPanel(
 | 
						|
                    "Image Inspector", (ImTextureID)(intptr_t)displayTexId,
 | 
						|
                    ImVec2(g_loadedImage.m_width, g_loadedImage.m_height),
 | 
						|
                    ImGuiTexInspect::InspectorFlags_NoTooltip |
 | 
						|
                        ImGuiTexInspect::InspectorFlags_NoGrid |
 | 
						|
                        ImGuiTexInspect::InspectorFlags_NoForceFilterNearest,
 | 
						|
                    ImGuiTexInspect::SizeIncludingBorder(availableContentSize));
 | 
						|
                ImGuiTexInspect::EndInspectorPanel();
 | 
						|
 | 
						|
                // --- Draw Crop Overlay If Active ---
 | 
						|
                if (g_cropActive && g_imageIsLoaded)
 | 
						|
                {
 | 
						|
 | 
						|
                    ImDrawList *drawList = ImGui::GetForegroundDrawList();
 | 
						|
                    ImGuiIO &io = ImGui::GetIO();
 | 
						|
                    ImVec2 mousePos = io.MousePos;
 | 
						|
 | 
						|
                    // Calculate screen coords of the current crop rectangle
 | 
						|
                    ImVec2 cropMinScreen =
 | 
						|
                        imageTopLeftScreen +
 | 
						|
                        ImVec2(g_cropRectNorm.x, g_cropRectNorm.y) * imageDisplaySize;
 | 
						|
                    ImVec2 cropMaxScreen =
 | 
						|
                        imageTopLeftScreen +
 | 
						|
                        ImVec2(g_cropRectNorm.z, g_cropRectNorm.w) * imageDisplaySize;
 | 
						|
                    ImVec2 cropSizeScreen = cropMaxScreen - cropMinScreen;
 | 
						|
 | 
						|
                    // Define handle size and interaction margin
 | 
						|
                    float handleScreenSize = 8.0f;
 | 
						|
                    float handleInteractionMargin =
 | 
						|
                        handleScreenSize * 1.5f;                    // Larger click area
 | 
						|
                    ImU32 colRect = IM_COL32(255, 255, 255, 200);   // White rectangle
 | 
						|
                    ImU32 colHandle = IM_COL32(255, 255, 255, 255); // Solid white handle
 | 
						|
                    ImU32 colGrid = IM_COL32(200, 200, 200, 100);   // Faint grid lines
 | 
						|
                    ImU32 colHover = IM_COL32(255, 255, 0, 255);    // Yellow highlight
 | 
						|
 | 
						|
                    // --- Define Handle Positions (screen coordinates) ---
 | 
						|
                    // Corners
 | 
						|
                    ImVec2 tl = cropMinScreen;
 | 
						|
                    ImVec2 tr = ImVec2(cropMaxScreen.x, cropMinScreen.y);
 | 
						|
                    ImVec2 bl = ImVec2(cropMinScreen.x, cropMaxScreen.y);
 | 
						|
                    ImVec2 br = cropMaxScreen;
 | 
						|
                    // Mid-edges
 | 
						|
                    ImVec2 tm = ImVec2((tl.x + tr.x) * 0.5f, tl.y);
 | 
						|
                    ImVec2 bm = ImVec2((bl.x + br.x) * 0.5f, bl.y);
 | 
						|
                    ImVec2 lm = ImVec2(tl.x, (tl.y + bl.y) * 0.5f);
 | 
						|
                    ImVec2 rm = ImVec2(tr.x, (tr.y + br.y) * 0.5f);
 | 
						|
 | 
						|
                    // Handle definitions for hit testing and drawing
 | 
						|
                    struct HandleDef
 | 
						|
                    {
 | 
						|
                        CropHandle id;
 | 
						|
                        ImVec2 pos;
 | 
						|
                    };
 | 
						|
                    HandleDef handles[] = {
 | 
						|
                        {CropHandle::TOP_LEFT, tl}, {CropHandle::TOP_RIGHT, tr}, {CropHandle::BOTTOM_LEFT, bl}, {CropHandle::BOTTOM_RIGHT, br}, {CropHandle::TOP, tm}, {CropHandle::BOTTOM, bm}, {CropHandle::LEFT, lm}, {CropHandle::RIGHT, rm}};
 | 
						|
 | 
						|
                    // --- Interaction Handling ---
 | 
						|
                    bool isHoveringAnyHandle = false;
 | 
						|
                    CropHandle hoveredHandle = CropHandle::NONE;
 | 
						|
 | 
						|
                    // Only interact if window is hovered
 | 
						|
                    if (ImGui::
 | 
						|
                            IsWindowHovered()) // ImGuiHoveredFlags_AllowWhenBlockedByActiveItem
 | 
						|
                                               // might also be needed
 | 
						|
                    {
 | 
						|
                        // Check handles first (higher priority than inside rect)
 | 
						|
                        for (const auto &h : handles)
 | 
						|
                        {
 | 
						|
                            ImRect handleRect(h.pos - ImVec2(handleInteractionMargin,
 | 
						|
                                                             handleInteractionMargin),
 | 
						|
                                              h.pos + ImVec2(handleInteractionMargin,
 | 
						|
                                                             handleInteractionMargin));
 | 
						|
                            if (handleRect.Contains(mousePos))
 | 
						|
                            {
 | 
						|
                                hoveredHandle = h.id;
 | 
						|
                                isHoveringAnyHandle = true;
 | 
						|
                                break;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
 | 
						|
                        // Check inside rect if no handle hovered
 | 
						|
                        ImRect insideRect(cropMinScreen, cropMaxScreen);
 | 
						|
                        if (!isHoveringAnyHandle && insideRect.Contains(mousePos))
 | 
						|
                        {
 | 
						|
                            hoveredHandle = CropHandle::INSIDE;
 | 
						|
                        }
 | 
						|
 | 
						|
                        // Mouse Down: Start dragging
 | 
						|
                        if (hoveredHandle != CropHandle::NONE &&
 | 
						|
                            ImGui::IsMouseClicked(ImGuiMouseButton_Left))
 | 
						|
                        {
 | 
						|
                            g_activeCropHandle = hoveredHandle;
 | 
						|
                            g_isDraggingCrop = true;
 | 
						|
                            g_dragStartMousePos = mousePos;
 | 
						|
                            g_cropRectNormInitial =
 | 
						|
                                g_cropRectNorm; // Store state at drag start
 | 
						|
                            printf("Started dragging handle: %d\n", (int)g_activeCropHandle);
 | 
						|
                        }
 | 
						|
                    } // End IsWindowHovered check
 | 
						|
 | 
						|
                    // Mouse Drag: Update crop rectangle
 | 
						|
                    if (g_isDraggingCrop &&
 | 
						|
                        ImGui::IsMouseDragging(ImGuiMouseButton_Left))
 | 
						|
                    {
 | 
						|
                        ImVec2 mouseDeltaScreen = mousePos - g_dragStartMousePos;
 | 
						|
                        // Convert delta to normalized image coordinates
 | 
						|
                        ImVec2 mouseDeltaNorm = ImVec2(0, 0);
 | 
						|
                        if (imageDisplaySize.x > 1e-3 &&
 | 
						|
                            imageDisplaySize.y > 1e-3)
 | 
						|
                        { // Avoid division by zero
 | 
						|
                            mouseDeltaNorm = mouseDeltaScreen / imageDisplaySize;
 | 
						|
                        }
 | 
						|
 | 
						|
                        // Update g_cropRectNorm based on handle and delta
 | 
						|
                        // Store temporary rect to apply constraints later
 | 
						|
                        ImVec4 tempRect =
 | 
						|
                            g_cropRectNormInitial; // Work from initial state + delta
 | 
						|
 | 
						|
                        // --- Update Logic (Needs Aspect Ratio Constraint Integration) ---
 | 
						|
                        // [This part is complex - Simplified version below]
 | 
						|
                        UpdateCropRect(tempRect, g_activeCropHandle, mouseDeltaNorm,
 | 
						|
                                       g_cropAspectRatio);
 | 
						|
 | 
						|
                        // Clamp final rect to 0-1 range and ensure min < max
 | 
						|
                        tempRect.x = ImClamp(tempRect.x, 0.0f, 1.0f);
 | 
						|
                        tempRect.y = ImClamp(tempRect.y, 0.0f, 1.0f);
 | 
						|
                        tempRect.z = ImClamp(tempRect.z, 0.0f, 1.0f);
 | 
						|
                        tempRect.w = ImClamp(tempRect.w, 0.0f, 1.0f);
 | 
						|
                        if (tempRect.x > tempRect.z)
 | 
						|
                            ImSwap(tempRect.x, tempRect.z);
 | 
						|
                        if (tempRect.y > tempRect.w)
 | 
						|
                            ImSwap(tempRect.y, tempRect.w);
 | 
						|
                        // Prevent zero size rect? (Optional)
 | 
						|
                        // float minSizeNorm = 0.01f; // e.g., 1% minimum size
 | 
						|
                        // if (tempRect.z - tempRect.x < minSizeNorm) tempRect.z =
 | 
						|
                        // tempRect.x + minSizeNorm; if (tempRect.w - tempRect.y <
 | 
						|
                        // minSizeNorm) tempRect.w = tempRect.y + minSizeNorm;
 | 
						|
 | 
						|
                        g_cropRectNorm = tempRect; // Update the actual state
 | 
						|
                    }
 | 
						|
                    else if (g_isDraggingCrop &&
 | 
						|
                             ImGui::IsMouseReleased(ImGuiMouseButton_Left))
 | 
						|
                    {
 | 
						|
                        // Mouse Release: Stop dragging
 | 
						|
                        g_isDraggingCrop = false;
 | 
						|
                        g_activeCropHandle = CropHandle::NONE;
 | 
						|
                        printf("Stopped dragging crop.\n");
 | 
						|
                    }
 | 
						|
 | 
						|
                    // --- Drawing ---
 | 
						|
                    // Dimming overlay (optional) - Draw 4 rects outside the crop area
 | 
						|
                    drawList->AddRectFilled(
 | 
						|
                        imageTopLeftScreen,
 | 
						|
                        ImVec2(cropMinScreen.x, imageBottomRightScreen.y),
 | 
						|
                        IM_COL32(0, 0, 0, 100)); // Left
 | 
						|
                    drawList->AddRectFilled(ImVec2(cropMaxScreen.x, imageTopLeftScreen.y),
 | 
						|
                                            imageBottomRightScreen,
 | 
						|
                                            IM_COL32(0, 0, 0, 100)); // Right
 | 
						|
                    drawList->AddRectFilled(ImVec2(cropMinScreen.x, imageTopLeftScreen.y),
 | 
						|
                                            ImVec2(cropMaxScreen.x, cropMinScreen.y),
 | 
						|
                                            IM_COL32(0, 0, 0, 100)); // Top
 | 
						|
                    drawList->AddRectFilled(
 | 
						|
                        ImVec2(cropMinScreen.x, cropMaxScreen.y),
 | 
						|
                        ImVec2(cropMaxScreen.x, imageBottomRightScreen.y),
 | 
						|
                        IM_COL32(0, 0, 0, 100)); // Bottom
 | 
						|
 | 
						|
                    // Draw crop rectangle outline
 | 
						|
                    drawList->AddRect(cropMinScreen, cropMaxScreen, colRect, 0.0f, 0,
 | 
						|
                                      1.5f);
 | 
						|
 | 
						|
                    // Draw grid lines (simple 3x3 grid)
 | 
						|
                    float thirdW = cropSizeScreen.x / 3.0f;
 | 
						|
                    float thirdH = cropSizeScreen.y / 3.0f;
 | 
						|
                    drawList->AddLine(ImVec2(cropMinScreen.x + thirdW, cropMinScreen.y),
 | 
						|
                                      ImVec2(cropMinScreen.x + thirdW, cropMaxScreen.y),
 | 
						|
                                      colGrid, 1.0f);
 | 
						|
                    drawList->AddLine(
 | 
						|
                        ImVec2(cropMinScreen.x + thirdW * 2, cropMinScreen.y),
 | 
						|
                        ImVec2(cropMinScreen.x + thirdW * 2, cropMaxScreen.y), colGrid,
 | 
						|
                        1.0f);
 | 
						|
                    drawList->AddLine(ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH),
 | 
						|
                                      ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH),
 | 
						|
                                      colGrid, 1.0f);
 | 
						|
                    drawList->AddLine(
 | 
						|
                        ImVec2(cropMinScreen.x, cropMinScreen.y + thirdH * 2),
 | 
						|
                        ImVec2(cropMaxScreen.x, cropMinScreen.y + thirdH * 2), colGrid,
 | 
						|
                        1.0f);
 | 
						|
 | 
						|
                    // Draw handles
 | 
						|
                    for (const auto &h : handles)
 | 
						|
                    {
 | 
						|
                        bool isHovered = (h.id == hoveredHandle);
 | 
						|
                        bool isActive = (h.id == g_activeCropHandle);
 | 
						|
                        drawList->AddRectFilled(
 | 
						|
                            h.pos - ImVec2(handleScreenSize / 2, handleScreenSize / 2),
 | 
						|
                            h.pos + ImVec2(handleScreenSize / 2, handleScreenSize / 2),
 | 
						|
                            (isHovered || isActive) ? colHover : colHandle);
 | 
						|
                    }
 | 
						|
                } // End if(g_cropActive)
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                // Show placeholder text if no image is loaded
 | 
						|
                ImVec2 winSize = ImGui::GetWindowSize();
 | 
						|
                ImVec2 textSize = ImGui::CalcTextSize("No Image Loaded");
 | 
						|
                ImGui::SetCursorPos(ImVec2((winSize.x - textSize.x) * 0.5f,
 | 
						|
                                           (winSize.y - textSize.y) * 0.5f));
 | 
						|
                ImGui::Text("No Image Loaded. File -> Open... to load an image");
 | 
						|
                std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0);
 | 
						|
                g_histogramMaxCount = 1;
 | 
						|
                // Or maybe: "File -> Open... to load an image"
 | 
						|
            }
 | 
						|
            ImGui::End(); // End Image View
 | 
						|
 | 
						|
            // "Image Exif" window
 | 
						|
            ImGui::Begin("Image Exif");
 | 
						|
            if (g_imageIsLoaded)
 | 
						|
            {
 | 
						|
                ImGui::Text("Image Width: %d", g_loadedImage.m_width);
 | 
						|
                ImGui::Text("Image Height: %d", g_loadedImage.m_height);
 | 
						|
                ImGui::Text("Image Loaded: %s", g_imageIsLoaded ? "Yes" : "No");
 | 
						|
                ImGui::Text("Image Channels: %d", g_loadedImage.m_channels);
 | 
						|
                ImGui::Text("Image Color Space: %s",
 | 
						|
                            g_loadedImage.m_colorSpaceName.c_str());
 | 
						|
                ImGui::Text("Image ICC Profile Size: %zu bytes",
 | 
						|
                            g_loadedImage.m_iccProfile.size());
 | 
						|
                ImGui::Text("Image Metadata Size: %zu bytes",
 | 
						|
                            g_loadedImage.m_metadata.size());
 | 
						|
                ImGui::Separator();
 | 
						|
                ImGui::Text("Image Metadata: ");
 | 
						|
                for (const auto &entry : g_loadedImage.m_metadata)
 | 
						|
                {
 | 
						|
                    ImGui::Text("%s: %s", entry.first.c_str(), entry.second.c_str());
 | 
						|
                }
 | 
						|
            } // Closing the if statement for g_imageIsLoaded
 | 
						|
            ImGui::End(); // End Image Exif
 | 
						|
 | 
						|
            // "Edit Image" window
 | 
						|
            ImGui::Begin("Edit Image");
 | 
						|
 | 
						|
            if (ImGui::CollapsingHeader("Histogram",
 | 
						|
                                        ImGuiTreeNodeFlags_DefaultOpen))
 | 
						|
            {
 | 
						|
                DrawHistogramWidget("ExifHistogram", ImVec2(-1, 256));
 | 
						|
            }
 | 
						|
 | 
						|
            // --- Edit Image (Right) ---
 | 
						|
            ImGui::Begin("Edit Image");
 | 
						|
 | 
						|
            // --- Pipeline Configuration ---
 | 
						|
            ImGui::SeparatorText("Processing Pipeline");
 | 
						|
 | 
						|
            // Input Color Space Selector
 | 
						|
            bool inputCsChanged = false;
 | 
						|
            ImGui::Text("Input Color Space:");
 | 
						|
            ImGui::SameLine();
 | 
						|
            if (ImGui::BeginCombo("##InputCS", ColorSpaceToString(g_inputColorSpace)))
 | 
						|
            {
 | 
						|
                if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_inputColorSpace == ColorSpace::LINEAR_SRGB))
 | 
						|
                {
 | 
						|
                    if (g_inputColorSpace != ColorSpace::LINEAR_SRGB)
 | 
						|
                        inputCsChanged = true;
 | 
						|
                    g_inputColorSpace = ColorSpace::LINEAR_SRGB;
 | 
						|
                }
 | 
						|
                if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_inputColorSpace == ColorSpace::SRGB))
 | 
						|
                {
 | 
						|
                    if (g_inputColorSpace != ColorSpace::SRGB)
 | 
						|
                        inputCsChanged = true;
 | 
						|
                    g_inputColorSpace = ColorSpace::SRGB;
 | 
						|
                }
 | 
						|
                ImGui::EndCombo();
 | 
						|
            }
 | 
						|
            if (inputCsChanged)
 | 
						|
                g_pipeline.MarkDirty();
 | 
						|
 | 
						|
            // --- Output Color Space Selector ---
 | 
						|
            bool outputCsChanged = false;
 | 
						|
            ImGui::Text("Output Color Space:");
 | 
						|
            ImGui::SameLine();
 | 
						|
            if (ImGui::BeginCombo("##OutputCS", ColorSpaceToString(g_outputColorSpace)))
 | 
						|
            {
 | 
						|
                if (ImGui::Selectable(ColorSpaceToString(ColorSpace::LINEAR_SRGB), g_outputColorSpace == ColorSpace::LINEAR_SRGB))
 | 
						|
                {
 | 
						|
                    if (g_outputColorSpace != ColorSpace::LINEAR_SRGB)
 | 
						|
                        outputCsChanged = true;
 | 
						|
                    g_outputColorSpace = ColorSpace::LINEAR_SRGB;
 | 
						|
                }
 | 
						|
                if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_outputColorSpace == ColorSpace::SRGB))
 | 
						|
                {
 | 
						|
                    if (g_outputColorSpace != ColorSpace::SRGB)
 | 
						|
                        outputCsChanged = true;
 | 
						|
                    g_outputColorSpace = ColorSpace::SRGB;
 | 
						|
                }
 | 
						|
                ImGui::EndCombo();
 | 
						|
            }
 | 
						|
            if (outputCsChanged)
 | 
						|
                g_pipeline.MarkDirty(); // <<< Mark dirty
 | 
						|
 | 
						|
            ImGui::Separator();
 | 
						|
            ImGui::Text("Operation Order:");
 | 
						|
 | 
						|
            // --- Drag-and-Drop Reordering List ---
 | 
						|
            int move_from = -1, move_to = -1;
 | 
						|
            bool order_or_enabled_changed = false; // Flag for changes in this section
 | 
						|
            for (int i = 0; i < g_pipeline.activeOperations.size(); ++i)
 | 
						|
            {
 | 
						|
                PipelineOperation &op = g_pipeline.activeOperations[i];
 | 
						|
                ImGui::PushID(i);
 | 
						|
                if (ImGui::Checkbox("", &op.enabled))
 | 
						|
                { // <<< Check return value
 | 
						|
                    order_or_enabled_changed = true;
 | 
						|
                }
 | 
						|
                // ... (Up/Down buttons - set order_or_enabled_changed = true if clicked) ...
 | 
						|
                ImGui::SameLine();
 | 
						|
                if (ImGui::ArrowButton("##up", ImGuiDir_Up) && i > 0)
 | 
						|
                {
 | 
						|
                    move_from = i;
 | 
						|
                    move_to = i - 1;
 | 
						|
                }
 | 
						|
                ImGui::SameLine();
 | 
						|
                if (ImGui::ArrowButton("##down", ImGuiDir_Down) && i < g_pipeline.activeOperations.size() - 1)
 | 
						|
                {
 | 
						|
                    move_from = i;
 | 
						|
                    move_to = i + 1;
 | 
						|
                }
 | 
						|
 | 
						|
                ImGui::SameLine();
 | 
						|
                ImGui::Selectable(op.name.c_str(), false, 0, ImVec2(ImGui::GetContentRegionAvail().x - 60, 0)); // Adjust size if needed
 | 
						|
 | 
						|
                // Drag Drop Source/Target
 | 
						|
                if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None))
 | 
						|
                { /* ... */
 | 
						|
                }
 | 
						|
                if (ImGui::BeginDragDropTarget())
 | 
						|
                {
 | 
						|
                    if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("PIPELINE_OP_DND"))
 | 
						|
                    {
 | 
						|
                        move_from = *(const int *)payload->Data;
 | 
						|
                        move_to = i;
 | 
						|
                    }
 | 
						|
                    ImGui::EndDragDropTarget();
 | 
						|
                }
 | 
						|
                ImGui::PopID();
 | 
						|
            }
 | 
						|
 | 
						|
            bool adjustment_changed = false;    // Still useful for general dirty marking
 | 
						|
            bool is_diffing_this_frame = false; // Track if *any* diff is active
 | 
						|
 | 
						|
            // Helper lambda to handle diff state logic after each slider
 | 
						|
            auto HandleSliderDiffState = [&](const char *operationName)
 | 
						|
            {
 | 
						|
                ImGuiIO &io = ImGui::GetIO();
 | 
						|
                bool slider_active = ImGui::IsItemActive();
 | 
						|
                bool alt_held = io.KeyAlt;
 | 
						|
 | 
						|
                if (slider_active)
 | 
						|
                {
 | 
						|
                    if (alt_held)
 | 
						|
                    {
 | 
						|
                        // Start or continue diffing for THIS operation
 | 
						|
                        g_pipeline.SetDiffActiveOperation(operationName);
 | 
						|
                        is_diffing_this_frame = true; // Mark that a diff is happening
 | 
						|
                        // MarkDirty is called within SetDiffActiveOperation if state changes
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        // Slider active, but Alt released OR was never pressed
 | 
						|
                        // If we *were* diffing this specific operation, stop it.
 | 
						|
                        if (g_pipeline.IsDiffActiveForOperation(operationName))
 | 
						|
                        {
 | 
						|
                            g_pipeline.SetDiffActiveOperation(nullptr);
 | 
						|
                            // MarkDirty called within SetDiffActiveOperation
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                }
 | 
						|
                else
 | 
						|
                {
 | 
						|
                    // Slider is NOT active. If we were diffing this specific operation, stop it.
 | 
						|
                    if (g_pipeline.IsDiffActiveForOperation(operationName))
 | 
						|
                    {
 | 
						|
                        g_pipeline.SetDiffActiveOperation(nullptr);
 | 
						|
                        // MarkDirty called within SetDiffActiveOperation
 | 
						|
                    }
 | 
						|
                }
 | 
						|
            };
 | 
						|
 | 
						|
            // Process move if detected
 | 
						|
            if (move_from != -1 && move_to != -1 && move_from != move_to)
 | 
						|
            {
 | 
						|
                // ... (Reordering logic) ...
 | 
						|
                order_or_enabled_changed = true; // <<< Reordering occurred
 | 
						|
            }
 | 
						|
 | 
						|
            if (order_or_enabled_changed)
 | 
						|
            {
 | 
						|
                g_pipeline.MarkDirty(); // <<< Mark dirty if enabled state or order changed
 | 
						|
            }
 | 
						|
 | 
						|
            if (ImGui::CollapsingHeader("White Balance", ImGuiTreeNodeFlags_DefaultOpen))
 | 
						|
            {
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Temperature", &temperature, 1000.0f, 20000.0f);
 | 
						|
                HandleSliderDiffState("White Balance"); // Assuming "White Balance" is the operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Tint", &tint, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("White Balance"); // Both sliders control the same operation
 | 
						|
            }
 | 
						|
            ImGui::Separator();
 | 
						|
            if (ImGui::CollapsingHeader("Tone", ImGuiTreeNodeFlags_DefaultOpen))
 | 
						|
            {
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
 | 
						|
                HandleSliderDiffState("Exposure"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Contrast", &contrast, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic);
 | 
						|
                HandleSliderDiffState("Contrast"); // Match operation name
 | 
						|
 | 
						|
                ImGui::Separator();
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Highlights", &highlights, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Highlights/Shadows"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Shadows", &shadows, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Highlights/Shadows"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Whites", &whites, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Whites/Blacks"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Blacks", &blacks, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Whites/Blacks"); // Match operation name
 | 
						|
            }
 | 
						|
            ImGui::Separator();
 | 
						|
            if (ImGui::CollapsingHeader("Presence", ImGuiTreeNodeFlags_DefaultOpen))
 | 
						|
            {
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Texture", &texture, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Texture"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Clarity", &clarity, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Clarity"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Dehaze", &dehaze, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Dehaze"); // Match operation name
 | 
						|
 | 
						|
                ImGui::Separator();
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Vibrance", &vibrance, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Vibrance"); // Match operation name
 | 
						|
 | 
						|
                adjustment_changed |= ImGui::SliderFloat("Saturation", &saturation, -100.0f, 100.0f);
 | 
						|
                HandleSliderDiffState("Saturation"); // Match operation name
 | 
						|
            }
 | 
						|
            ImGui::Separator();
 | 
						|
 | 
						|
            // If any non-diff adjustment happened, mark dirty
 | 
						|
            // (SetDiffActiveOperation already marks dirty if the diff state *changes*)
 | 
						|
            if (adjustment_changed && !is_diffing_this_frame)
 | 
						|
            {
 | 
						|
                g_pipeline.MarkDirty();
 | 
						|
            }
 | 
						|
 | 
						|
            ImGui::SeparatorText("Transform");
 | 
						|
            bool crop_applied_or_cancelled = false;
 | 
						|
            if (!g_cropActive)
 | 
						|
            {
 | 
						|
                if (ImGui::Button("Crop & Straighten"))
 | 
						|
                { // Combine visually for now
 | 
						|
                    g_cropActive = true;
 | 
						|
                    g_cropRectNorm =
 | 
						|
                        ImVec4(0.0f, 0.0f, 1.0f, 1.0f);     // Reset crop on activation
 | 
						|
                    g_cropRectNormInitial = g_cropRectNorm; // Store initial state
 | 
						|
                    g_activeCropHandle = CropHandle::NONE;
 | 
						|
                    g_isDraggingCrop = false;
 | 
						|
 | 
						|
                    // Update Original aspect ratio if needed
 | 
						|
                    if (g_loadedImage.getHeight() > 0)
 | 
						|
                    {
 | 
						|
                        for (auto &opt : g_aspectRatios)
 | 
						|
                        {
 | 
						|
                            if (strcmp(opt.name, "Original") == 0)
 | 
						|
                            {
 | 
						|
                                opt.ratio = float(g_loadedImage.getWidth()) /
 | 
						|
                                            float(g_loadedImage.getHeight());
 | 
						|
                                break;
 | 
						|
                            }
 | 
						|
                        }
 | 
						|
                    }
 | 
						|
                    // If current selection is 'Original', update g_cropAspectRatio
 | 
						|
                    if (g_selectedAspectRatioIndex >= 0 &&
 | 
						|
                        g_selectedAspectRatioIndex < g_aspectRatios.size() &&
 | 
						|
                        strcmp(g_aspectRatios[g_selectedAspectRatioIndex].name,
 | 
						|
                               "Original") == 0)
 | 
						|
                    {
 | 
						|
                        g_cropAspectRatio =
 | 
						|
                            g_aspectRatios[g_selectedAspectRatioIndex].ratio;
 | 
						|
                    }
 | 
						|
 | 
						|
                    printf("Crop tool activated.\n");
 | 
						|
                }
 | 
						|
            }
 | 
						|
            else
 | 
						|
            {
 | 
						|
                ImGui::Text("Crop Active");
 | 
						|
 | 
						|
                // Aspect Ratio Selector
 | 
						|
                if (ImGui::BeginCombo(
 | 
						|
                        "Aspect Ratio",
 | 
						|
                        g_aspectRatios[g_selectedAspectRatioIndex].name))
 | 
						|
                {
 | 
						|
                    for (int i = 0; i < g_aspectRatios.size(); ++i)
 | 
						|
                    {
 | 
						|
                        bool is_selected = (g_selectedAspectRatioIndex == i);
 | 
						|
                        if (ImGui::Selectable(g_aspectRatios[i].name, is_selected))
 | 
						|
                        {
 | 
						|
                            g_selectedAspectRatioIndex = i;
 | 
						|
                            g_cropAspectRatio = g_aspectRatios[i].ratio;
 | 
						|
                            // Optional: Reset crop rectangle slightly or adjust existing one
 | 
						|
                            // to the new ratio if transitioning from freeform? Or just let
 | 
						|
                            // user resize.
 | 
						|
                            printf("Selected aspect ratio: %s (%.2f)\n",
 | 
						|
                                   g_aspectRatios[i].name, g_cropAspectRatio);
 | 
						|
                        }
 | 
						|
                        if (is_selected)
 | 
						|
                            ImGui::SetItemDefaultFocus();
 | 
						|
                    }
 | 
						|
                    ImGui::EndCombo();
 | 
						|
                }
 | 
						|
 | 
						|
                // Apply/Cancel Buttons
 | 
						|
                if (ImGui::Button("Apply Crop"))
 | 
						|
                {
 | 
						|
                    printf("Apply Crop button clicked.\n");
 | 
						|
                    // <<< --- CALL FUNCTION TO APPLY CROP --- >>>
 | 
						|
                    if (ApplyCropToImage(g_loadedImage, g_cropRectNorm))
 | 
						|
                    {
 | 
						|
                        printf("Crop applied successfully. Reloading texture and resetting "
 | 
						|
                               "pipeline.\n");
 | 
						|
                        // Reload texture with cropped data
 | 
						|
                        if (!loadImageTexture(g_loadedImage))
 | 
						|
                        {
 | 
						|
                            fprintf(stderr, "Error reloading texture after crop!\n");
 | 
						|
                            g_imageIsLoaded = false; // Mark as not usable
 | 
						|
                        }
 | 
						|
                        // Reset pipeline FBOs/Textures due to size change
 | 
						|
                        g_pipeline.ResetResources();
 | 
						|
                        crop_applied_or_cancelled = true;
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        fprintf(stderr, "Failed to apply crop to image data.\n");
 | 
						|
                        // Optionally show error to user
 | 
						|
                    }
 | 
						|
                    // Reset state after applying
 | 
						|
                    g_cropActive = false;
 | 
						|
                    g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f);
 | 
						|
                    g_activeCropHandle = CropHandle::NONE;
 | 
						|
                    g_isDraggingCrop = false;
 | 
						|
                }
 | 
						|
                ImGui::SameLine();
 | 
						|
                if (ImGui::Button("Cancel Crop"))
 | 
						|
                {
 | 
						|
                    printf("Crop cancelled.\n");
 | 
						|
                    g_cropActive = false;
 | 
						|
                    crop_applied_or_cancelled = true;
 | 
						|
                    g_cropRectNorm =
 | 
						|
                        ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset to full image
 | 
						|
                    g_activeCropHandle = CropHandle::NONE;
 | 
						|
                    g_isDraggingCrop = false;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            ImGui::End(); // End Edit Image
 | 
						|
 | 
						|
            ImGui::End(); // End MainDockspaceWindow
 | 
						|
        }
 | 
						|
        else
 | 
						|
        {
 | 
						|
            // Option 2: Simple full-screen window (no docking)
 | 
						|
            ImGuiViewport *viewport = ImGui::GetMainViewport();
 | 
						|
            ImGui::SetNextWindowPos(viewport->WorkPos);
 | 
						|
            ImGui::SetNextWindowSize(viewport->WorkSize);
 | 
						|
            ImGuiWindowFlags window_flags =
 | 
						|
                ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
 | 
						|
                ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoSavedSettings |
 | 
						|
                ImGuiWindowFlags_NoBringToFrontOnFocus;
 | 
						|
            ImGui::Begin("FullImageViewer", nullptr, window_flags);
 | 
						|
            ImGui::Text("Image Viewer");
 | 
						|
            ImGuiTexInspect::BeginInspectorPanel(
 | 
						|
                "Image Inspector", g_loadedImage.m_textureId,
 | 
						|
                ImVec2(g_loadedImage.m_width, g_loadedImage.m_height),
 | 
						|
                ImGuiTexInspect::InspectorFlags_NoTooltip);
 | 
						|
            ImGuiTexInspect::EndInspectorPanel();
 | 
						|
            ImGui::End();
 | 
						|
        }
 | 
						|
 | 
						|
        // Rendering
 | 
						|
        ImGui::Render();
 | 
						|
        glViewport(0, 0, (int)io.DisplaySize.x, (int)io.DisplaySize.y);
 | 
						|
        glClearColor(clear_color.x * clear_color.w, clear_color.y * clear_color.w,
 | 
						|
                     clear_color.z * clear_color.w, clear_color.w);
 | 
						|
        glClear(GL_COLOR_BUFFER_BIT);
 | 
						|
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
 | 
						|
 | 
						|
        // Update and Render additional Platform Windows
 | 
						|
        // (Platform functions may change the current OpenGL context, so we
 | 
						|
        // save/restore it to make it easier to paste this code elsewhere.
 | 
						|
        //  For this specific demo app we could also call SDL_GL_MakeCurrent(window,
 | 
						|
        //  gl_context) directly)
 | 
						|
        if (io.ConfigFlags & ImGuiConfigFlags_ViewportsEnable)
 | 
						|
        {
 | 
						|
            SDL_Window *backup_current_window = SDL_GL_GetCurrentWindow();
 | 
						|
            SDL_GLContext backup_current_context = SDL_GL_GetCurrentContext();
 | 
						|
            ImGui::UpdatePlatformWindows();
 | 
						|
            ImGui::RenderPlatformWindowsDefault();
 | 
						|
            SDL_GL_MakeCurrent(backup_current_window, backup_current_context);
 | 
						|
        }
 | 
						|
 | 
						|
        SDL_GL_SwapWindow(window);
 | 
						|
    }
 | 
						|
#ifdef __EMSCRIPTEN__
 | 
						|
    EMSCRIPTEN_MAINLOOP_END;
 | 
						|
#endif
 | 
						|
 | 
						|
    // Cleanup
 | 
						|
    // --- Cleanup ---
 | 
						|
    // Destroy operations which will delete shader programs
 | 
						|
    g_allOperations
 | 
						|
        .clear(); // Deletes PipelineOperation objects and their shaders
 | 
						|
    g_pipeline.activeOperations
 | 
						|
        .clear(); // Clear the list in pipeline (doesn't own shaders)
 | 
						|
    // Pipeline destructor handles FBOs/VAO etc.
 | 
						|
 | 
						|
    // Delete the originally loaded texture
 | 
						|
    if (g_loadedImage.m_textureId != 0)
 | 
						|
    {
 | 
						|
        glDeleteTextures(1, &g_loadedImage.m_textureId);
 | 
						|
        g_loadedImage.m_textureId = 0;
 | 
						|
    }
 | 
						|
 | 
						|
    if (g_histogramResourcesInitialized)
 | 
						|
    {
 | 
						|
        if (g_histogramSSBO)
 | 
						|
            glDeleteBuffers(1, &g_histogramSSBO);
 | 
						|
        if (g_histogramComputeShader)
 | 
						|
            glDeleteProgram(g_histogramComputeShader);
 | 
						|
        printf("Cleaned up histogram resources.\n");
 | 
						|
    }
 | 
						|
 | 
						|
    ImGuiTexInspect::Shutdown();
 | 
						|
 | 
						|
    ImGui_ImplOpenGL3_Shutdown();
 | 
						|
    ImGui_ImplSDL2_Shutdown();
 | 
						|
    ImGui::DestroyContext();
 | 
						|
 | 
						|
    SDL_GL_DeleteContext(gl_context);
 | 
						|
    SDL_DestroyWindow(window);
 | 
						|
    SDL_Quit();
 | 
						|
 | 
						|
    return 0;
 | 
						|
}
 |