diff --git a/Makefile b/Makefile index 1978428..e45c8ad 100644 --- a/Makefile +++ b/Makefile @@ -23,9 +23,9 @@ OBJS = $(addsuffix .o, $(basename $(notdir $(SOURCES)))) UNAME_S := $(shell uname -s) LINUX_GL_LIBS = -lGLEW -lGL -ldl -CXXFLAGS = -std=c++17 -I$(IMGUI_DIR) -I$(IMGUI_DIR)/backends +CXXFLAGS = -std=c++17 -I$(IMGUI_DIR) -I$(IMGUI_DIR)/backends -DIMGUI_DEFINE_MATH_OPERATORS CXXFLAGS += -g -Wall -Wformat -O3 -LIBS = -lraw -ljpeg -lpng -ltiff -lz -lm -DIMGUI -DIMGUI_DEFINE_MATH_OPERATORS +LIBS = -lraw -ljpeg -lpng -ltiff -lz -lm -DIMGUI -DIMGUI_DEFINE_MATH_OPERATORS -DCUSTOM_IN_APP_GPU_PROFILER_CONFIG=iagpConfig.h ##--------------------------------------------------------------------- diff --git a/imgui.ini b/imgui.ini index 0c0f7dd..6f1a5ce 100644 --- a/imgui.ini +++ b/imgui.ini @@ -1,24 +1,24 @@ [Window][Image Exif] Pos=0,19 -Size=501,2072 +Size=233,1454 Collapsed=0 -DockId=0x00000003,0 +DockId=0x00000005,0 [Window][Edit Image] -Pos=3308,19 -Size=532,2072 +Pos=2086,19 +Size=324,1454 Collapsed=0 DockId=0x00000002,0 [Window][Image View] -Pos=503,19 -Size=2803,2072 +Pos=235,19 +Size=1849,1454 Collapsed=0 DockId=0x00000004,0 [Window][DockSpaceWindowHost] Pos=0,19 -Size=3840,2072 +Size=2410,1454 Collapsed=0 [Window][Debug##Default] @@ -276,10 +276,58 @@ Pos=290,135 Size=700,450 Collapsed=0 -[Docking][Data] -DockSpace ID=0xE098E157 Window=0x9E772337 Pos=0,19 Size=3840,2072 Split=X - DockNode ID=0x00000001 Parent=0xE098E157 SizeRef=3306,720 Split=X - DockNode ID=0x00000003 Parent=0x00000001 SizeRef=501,720 Selected=0x5593B2D4 - DockNode ID=0x00000004 Parent=0x00000001 SizeRef=2803,720 CentralNode=1 Selected=0x9B39CB70 - DockNode ID=0x00000002 Parent=0xE098E157 SizeRef=532,720 Selected=0x610DAB84 +[Window][Open Image File##filebrowser_109471027310912] +Pos=861,431 +Size=700,450 +Collapsed=0 + +[Window][###ProfilerWindow] +Pos=0,1240 +Size=671,851 +Collapsed=0 +DockId=0x00000006,0 + +[Window][Open Image File##filebrowser_103987528614208] +Pos=1000,385 +Size=700,450 +Collapsed=0 + +[Window][Open Image File##filebrowser_102981197480256] +Pos=1133,650 +Size=700,450 +Collapsed=0 + +[Window][Export Image As##filebrowser_102981197479776] +Pos=1570,820 +Size=700,450 +Collapsed=0 + +[Window][Open Image File##filebrowser_111150851768640] +Pos=1570,820 +Size=700,450 +Collapsed=0 + +[Window][Open Image File##filebrowser_104296741056832] +Pos=1570,820 +Size=700,450 +Collapsed=0 + +[Window][Open Image File##filebrowser_111251638272320] +Pos=290,135 +Size=700,450 +Collapsed=0 + +[Window][Open Image File##filebrowser_94747345662272] +Pos=290,135 +Size=700,450 +Collapsed=0 + +[Docking][Data] +DockSpace ID=0xE098E157 Window=0x9E772337 Pos=0,19 Size=2410,1454 Split=X + DockNode ID=0x00000001 Parent=0xE098E157 SizeRef=1594,720 Split=X + DockNode ID=0x00000003 Parent=0x00000001 SizeRef=233,720 Split=Y Selected=0x5593B2D4 + DockNode ID=0x00000005 Parent=0x00000003 SizeRef=501,706 Selected=0x5593B2D4 + DockNode ID=0x00000006 Parent=0x00000003 SizeRef=501,493 Selected=0x00DB98BF + DockNode ID=0x00000004 Parent=0x00000001 SizeRef=1359,720 CentralNode=1 Selected=0x9B39CB70 + DockNode ID=0x00000002 Parent=0xE098E157 SizeRef=324,720 Selected=0x610DAB84 diff --git a/llm.md b/llm.md index 781c38f..32d2f3c 100644 --- a/llm.md +++ b/llm.md @@ -1,3 +1,4 @@ +main.cpp ``` // 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.) @@ -9,7 +10,7 @@ // - Introduction, links and more at the top of imgui.cpp #define IMGUI_DEFINE_MATH_OPERATORS - +#include #include "imgui.h" #include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" @@ -19,7 +20,7 @@ #include #else #include - +#include #endif // This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details. @@ -35,7 +36,7 @@ #include "app_image.h" #include "tex_inspect_opengl.h" #include "imgui_tex_inspect.h" - +#include "shaderutils.h" static float exposure = 0.0f; static float contrast = 0.0f; @@ -49,7 +50,1181 @@ 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; +static float dehaze = 0.0f; + +#include +#include +#include +#include // For std::function +#include // For unique_ptr + +#include "imfilebrowser.h" // <<< Add this +#include // <<< 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 uniforms; // Map uniform name to its info + + // Function to update uniforms based on global slider values etc. + std::function 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 &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; + + 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"); + } + + void CreateOrResizeFBOs(int width, int height) + { + if (width == m_texWidth && height == m_texHeight && m_fbo[0] != 0) + { + return; // Already correct size + } + + if (width <= 0 || height <= 0) + return; // Invalid dimensions + + // Cleanup existing + DestroyFBOs(); + + m_texWidth = width; + m_texHeight = 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); // Or GL_FRAMEBUFFER_BINDING + + for (int i = 0; i < 2; ++i) + { + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[i]); + glBindTexture(GL_TEXTURE_2D, m_tex[i]); + + // Create floating point texture + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, width, height, 0, GL_RGBA, GL_FLOAT, nullptr); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // Use NEAREST for processing steps + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + // Attach texture to FBO + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex[i], 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + fprintf(stderr, "ERROR::FRAMEBUFFER:: Framebuffer %d is not complete!\n", i); + DestroyFBOs(); // Clean up partial setup + glBindTexture(GL_TEXTURE_2D, lastTexture); + glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); + return; + } + 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); + } + + void DestroyFBOs() + { + if (m_fbo[0]) + glDeleteFramebuffers(2, m_fbo); + if (m_tex[0]) + glDeleteTextures(2, m_tex); + m_fbo[0] = m_fbo[1] = 0; + m_tex[0] = m_tex[1] = 0; + m_texWidth = m_texHeight = 0; + printf("Destroyed FBOs and textures.\n"); + } + +public: + // The ordered list of operations the user has configured + std::vector activeOperations; + ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default based on AppImage goal + ColorSpace outputColorSpace = ColorSpace::SRGB; // Default for display + + ImageProcessingPipeline() = default; + + ~ImageProcessingPipeline() + { + DestroyFBOs(); + 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 + if (m_passthroughShader) + glDeleteProgram(m_passthroughShader); + if (m_linearToSrgbShader) + glDeleteProgram(m_linearToSrgbShader); + if (m_srgbToLinearShader) + glDeleteProgram(m_srgbToLinearShader); + printf("ImageProcessingPipeline destroyed.\n"); + } + + void Init(const std::string &shaderBasePath) + { + printf("Initializing ImageProcessingPipeline...\n"); + CreateFullscreenQuad(); + printf("Fullscreen quad created.\n"); + // Load essential shaders + std::string vsPath = shaderBasePath + "passthrough.vert"; + printf("Loading shaders from: %s\n", vsPath.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"); + printf("Loaded shaders: %s, %s, %s\n", vsPath.c_str(), (shaderBasePath + "linear_to_srgb.frag").c_str(), (shaderBasePath + "srgb_to_linear.frag").c_str()); + + if (!m_passthroughShader || !m_linearToSrgbShader || !m_srgbToLinearShader) + { + fprintf(stderr, "Failed to load essential pipeline shaders!\n"); + } + else + { + printf("Essential pipeline shaders loaded.\n"); + } + } + + void ResetResources() + { + printf("Pipeline: Resetting FBOs and Textures.\n"); + DestroyFBOs(); // Call the existing cleanup method + } + + // Call this each frame to process the image + // Returns the Texture ID of the final processed image + GLuint ProcessImage(GLuint inputTextureId, int width, int height, bool applyOutputConversion = true) + { + if (inputTextureId == 0 || width <= 0 || height <= 0) + { + return 0; // No input or invalid size + } + + CreateOrResizeFBOs(width, height); + if (m_fbo[0] == 0) + { + fprintf(stderr, "FBOs not ready, cannot process image.\n"); + return 0; // FBOs not ready + } + + // Store original viewport and FBO to restore later + GLint viewport[4]; + glGetIntegerv(GL_VIEWPORT, viewport); + GLint lastFBO; + glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); + + glViewport(0, 0, m_texWidth, m_texHeight); + glBindVertexArray(m_vao); // Bind the quad VAO once + + int currentSourceTexIndex = 0; // Start with texture m_tex[0] as the first *write* target + GLuint currentReadTexId = inputTextureId; // Initially read from the original image + + // --- Input Color Space Conversion --- + bool inputConversionDone = false; + if (inputColorSpace == ColorSpace::SRGB) + { + printf("Pipeline: Applying sRGB -> Linear conversion.\n"); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); + glUseProgram(m_srgbToLinearShader); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_1D, currentReadTexId); + glUniform1i(glGetUniformLocation(m_srgbToLinearShader, "InputTexture"), 0); + glDrawArrays(GL_TRIANGLES, 0, 6); + + currentReadTexId = m_tex[currentSourceTexIndex]; // Next read is from the texture we just wrote to + currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap target FBO/texture + inputConversionDone = true; + } + else + { + printf("Pipeline: Input is Linear, no conversion needed.\n"); + // If input is already linear, we might need to copy it to the first FBO texture + // if there are actual processing steps, otherwise the first step reads the original. + // This copy ensures the ping-pong works correctly even if the first *user* step is disabled. + // However, if NO user steps are enabled, we want to display the original (potentially with output conversion). + bool anyUserOpsEnabled = false; + for (const auto &op : activeOperations) + { + if (op.enabled && op.shaderProgram && op.name != "Passthrough") + { // Check it's a real operation + anyUserOpsEnabled = true; + break; + } + } + + if (anyUserOpsEnabled) + { + // Need to copy original linear input into the pipeline's texture space + printf("Pipeline: Copying linear input to FBO texture for processing.\n"); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); + glUseProgram(m_passthroughShader); // Use simple passthrough + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, currentReadTexId); + glUniform1i(glGetUniformLocation(m_passthroughShader, "InputTexture"), 0); + glDrawArrays(GL_TRIANGLES, 0, 6); + currentReadTexId = m_tex[currentSourceTexIndex]; + currentSourceTexIndex = 1 - currentSourceTexIndex; + inputConversionDone = true; + } + else + { + // No user ops, keep reading directly from original inputTextureId + inputConversionDone = false; // Treat as if no initial step happened yet + printf("Pipeline: No enabled user operations, skipping initial copy.\n"); + } + } + + // --- Apply Editing Operations --- + int appliedOps = 0; + for (const auto &op : activeOperations) + { + if (op.enabled && op.shaderProgram) + { + printf("Pipeline: Applying operation: %s\n", op.name.c_str()); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); + glUseProgram(op.shaderProgram); + + // Set Input Texture Sampler + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, currentReadTexId); + GLint loc = glGetUniformLocation(op.shaderProgram, "InputTexture"); + if (loc != -1) + glUniform1i(loc, 0); + else if (op.name != "Passthrough") + fprintf(stderr, "Warning: InputTexture uniform not found in shader %s\n", op.name.c_str()); + + // Set operation-specific uniforms + if (op.updateUniformsCallback) + { + op.updateUniformsCallback(op.shaderProgram); + } + else + { + // Alternative: Set uniforms directly based on stored pointers + 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.clarityVal && op.uniforms.count("clarityValue")) + { + glUniform1f(op.uniforms.at("clarityValue").location, *op.clarityVal); + } + 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.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); + } + if (op.saturationVal && op.uniforms.count("saturationValue")) + { + glUniform1f(op.uniforms.at("saturationValue").location, *op.saturationVal); + } + if (op.vibranceVal && op.uniforms.count("vibranceValue")) + { + glUniform1f(op.uniforms.at("vibranceValue").location, *op.vibranceVal); + } + 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); + } + } + + glDrawArrays(GL_TRIANGLES, 0, 6); + + // Prepare for next pass + currentReadTexId = m_tex[currentSourceTexIndex]; // Next pass reads from the texture we just wrote + currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap FBO target + appliedOps++; + } + } + + // If no user ops were applied AND no input conversion happened, + // currentReadTexId is still the original inputTextureId. + if (appliedOps == 0 && !inputConversionDone) + { + printf("Pipeline: No operations applied, output = input (%d).\n", currentReadTexId); + // Proceed to output conversion using original inputTextureId + } + else if (appliedOps > 0 || inputConversionDone) + { + printf("Pipeline: %d operations applied, final intermediate texture ID: %d\n", appliedOps, currentReadTexId); + // currentReadTexId now holds the result of the last applied operation (or the input conversion) + } + else + { + // This case should ideally not be reached if logic above is correct + printf("Pipeline: Inconsistent state after processing loop.\n"); + } + + // --- Output Color Space Conversion --- + GLuint finalTextureId = currentReadTexId; // Assume this is the final one unless converted + if (applyOutputConversion) + { + if (outputColorSpace == ColorSpace::SRGB) + { + // Check if the last written data (currentReadTexId) is already sRGB. + // In this simple setup, it's always linear *unless* no ops applied and input was sRGB. + // More robustly: Track the color space through the pipeline. + // For now, assume currentReadTexId holds linear data if any op or input conversion happened. + bool needsLinearToSrgb = (appliedOps > 0 || inputConversionDone); + + if (!needsLinearToSrgb && inputColorSpace == ColorSpace::SRGB) + { + printf("Pipeline: Output is sRGB, and input was sRGB with no ops, no final conversion needed.\n"); + // Input was sRGB, no ops applied, output should be sRGB. currentReadTexId is original sRGB input. + finalTextureId = currentReadTexId; + } + else if (needsLinearToSrgb) + { + printf("Pipeline: Applying Linear -> sRGB conversion for output.\n"); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); // Use the *next* FBO for the final write + glUseProgram(m_linearToSrgbShader); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read the last result + glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); + glDrawArrays(GL_TRIANGLES, 0, 6); + finalTextureId = m_tex[currentSourceTexIndex]; // The final result is in this texture + } + else + { + // Input was linear, no ops, output requires sRGB. + printf("Pipeline: Input Linear, no ops, applying Linear -> sRGB conversion for output.\n"); + glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); + glUseProgram(m_linearToSrgbShader); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read original linear input + glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); + glDrawArrays(GL_TRIANGLES, 0, 6); + finalTextureId = m_tex[currentSourceTexIndex]; + } + } + else + { + printf("Pipeline: Output is Linear, no final conversion needed.\n"); + // If output should be linear, finalTextureId is already correct (it's currentReadTexId) + finalTextureId = currentReadTexId; + } + } + else + { + printf("Pipeline: Skipped output conversion. Final (linear) ID: %d\n", finalTextureId); + } + + // --- Cleanup --- + glBindVertexArray(0); + glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); // Restore original framebuffer binding + glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); // Restore viewport + glUseProgram(0); // Unbind shader program + + printf("Pipeline: ProcessImage returning final texture ID: %d\n", finalTextureId); + return finalTextureId; + } +}; + +static ImageProcessingPipeline g_pipeline; // <<< Global pipeline manager instance +static std::vector> 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 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 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 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 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(round(clampedRect.x * srcW)); + int cropY_px = static_cast(round(clampedRect.y * srcH)); + int cropMaxX_px = static_cast(round(clampedRect.z * srcW)); + int cropMaxY_px = static_cast(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(y_src) * srcW + cropX_px) * channels; + float* dstRowStart = dstData + (static_cast(y_dst) * cropW_px) * channels; + + // Copy the entire row (width * channels floats) + std::memcpy(dstRowStart, srcRowStart, static_cast(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("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("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("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("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("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("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("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("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("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("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 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 **) @@ -119,6 +1294,16 @@ int main(int, char **) 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(); @@ -150,34 +1335,36 @@ int main(int, char **) // 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; - - std::optional imgOpt = loadImage("testraw.arw"); - if (imgOpt) - { - g_loadedImage = std::move(*imgOpt); // Store the loaded image data - if (loadImageTexture(g_loadedImage)) - { - // Load the image into the viewer - g_imageIsLoaded = true; - std::cout << "Image loaded into viewer successfully." << std::endl; - } - else - { - g_imageIsLoaded = false; - std::cerr << "Failed to load image into viewer." << std::endl; - } - } - else - { - 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__ @@ -214,113 +1401,684 @@ int main(int, char **) ImGui_ImplSDL2_NewFrame(); ImGui::NewFrame(); - if (ImGui::BeginMainMenuBar()) { - if (ImGui::BeginMenu("File")) { - if (ImGui::MenuItem("Open...", "Ctrl+O")) { /* Do something */ } - if (ImGui::MenuItem("Save", "Ctrl+S")) { /* Do something */ } - if (ImGui::MenuItem("Save As...", "Ctrl+Shift+S")) { /* Do something */ } + 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) + { + g_pipeline.inputColorSpace = g_inputColorSpace; + g_pipeline.outputColorSpace = g_outputColorSpace; + + // Modify pipeline processing slightly to get both display and save textures + // Add a flag or method to control output conversion for saving + textureToSave = g_pipeline.ProcessImage( + g_loadedImage.m_textureId, + g_loadedImage.getWidth(), + g_loadedImage.getHeight(), + false // <-- Add argument: bool applyOutputConversion = true + ); + textureToDisplay = g_pipeline.ProcessImage( + g_loadedImage.m_textureId, + g_loadedImage.getWidth(), + g_loadedImage.getHeight(), + true // Apply conversion for display + ); + // If the pipeline wasn't modified, textureToSave might need extra work + } + else + { + textureToDisplay = 0; + textureToSave = 0; + } + + // --- 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("Export...", "Ctrl+E")) { /* Do something */ } - ImGui::Separator(); - if (ImGui::MenuItem("Exit")) { - // Need access to the window pointer to close it cleanly - // glfwSetWindowShouldClose(window_ptr, true); // How to get window_ptr here? Pass it or use a global/singleton. - // For simplicity now, just print. - std::cout << "Exit selected (implement window closing)" << std::endl; + if (ImGui::MenuItem("Exit")) + { + done = true; // Simple exit for now } ImGui::EndMenu(); } - if (ImGui::BeginMenu("Edit")) { - if (ImGui::MenuItem("Undo", "Ctrl+Z")) { /* Do something */ } - if (ImGui::MenuItem("Redo", "Ctrl+Y")) { /* Do something */ } - ImGui::Separator(); - if (ImGui::MenuItem("Copy Settings")) { /* Do something */ } - if (ImGui::MenuItem("Paste Settings")) { /* Do something */ } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("View")) { - if (ImGui::MenuItem("Zoom In", "+")) { /* Do something */ } - if (ImGui::MenuItem("Zoom Out", "-")) { /* Do something */ } - if (ImGui::MenuItem("Fit to Screen", "0")) { /* Do something */ } - ImGui::EndMenu(); - } - if (ImGui::BeginMenu("Help")) { - if (ImGui::MenuItem("About")) { /* Do something */ } - 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 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); - ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | - ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove; - window_flags |= ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus; - window_flags |= ImGuiWindowFlags_NoBackground; // Let DockSpace handle background - ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); - ImGui::Begin("MainDockspaceWindow", &use_dockspace, window_flags); - ImGui::PopStyleVar(3); // WindowPadding, WindowBorderSize, WindowRounding + ImGui::Begin("DockSpaceWindowHost", nullptr, host_window_flags); // No bool* needed + ImGui::PopStyleVar(3); - ImGuiID dockspace_id = ImGui::GetID("MyDockSpace"); - ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); + // Create the actual dockspace area. + ImGui::DockSpace(dockspace_id, ImVec2(0.0f, 0.0f), dockspace_flags); - // Setup a docking layout: Left = "Image Exif", Right = "Edit Image", Center = "Image View" - if (!ImGui::DockBuilderGetNode(dockspace_id)) + 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()) { - ImGui::DockBuilderRemoveNode(dockspace_id); + 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); - ImGuiID dock_main_id = dockspace_id; - ImGuiID dock_left_id, dock_right_id, dock_center_id; - // Split off 20% on the left for Image Exif. - ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Left, 0.2f, &dock_left_id, &dock_main_id); - // Split off 20% on the right for Edit Image. - ImGui::DockBuilderSplitNode(dock_main_id, ImGuiDir_Right, 0.2f, &dock_right_id, &dock_center_id); - // Dock the windows into their panels. + 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); + 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 --- - // "Image View" window in the center. + // --- 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"); - ImVec2 contentSize = ImGui::GetContentRegionAvail(); - ImGuiTexInspect::BeginInspectorPanel("Image Inspector", g_loadedImage.m_textureId, - ImVec2(g_loadedImage.m_width, g_loadedImage.m_height), - ImGuiTexInspect::InspectorFlags_NoTooltip | - ImGuiTexInspect::InspectorFlags_NoGrid | - ImGuiTexInspect::InspectorFlags_NoForceFilterNearest, - ImGuiTexInspect::SizeIncludingBorder(contentSize)); - ImGuiTexInspect::EndInspectorPanel(); - ImGui::End(); + // 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 - // "Image Exif" window docked at the left. - ImGui::Begin("Image Exif"); - 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::Text("Image Metadata: "); - for (const auto &entry : g_loadedImage.m_metadata) + GLuint displayTexId = textureToDisplay; // Use the display texture ID + if (displayTexId != 0) { - ImGui::Text("%s: %s", entry.first.c_str(), entry.second.c_str()); - } - ImGui::End(); + ComputeHistogramGPU(textureToDisplay, g_loadedImage.getWidth(), g_loadedImage.getHeight()); + // 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()); - // "Edit Image" window docked at the right. + 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 + 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)) + { + g_inputColorSpace = ColorSpace::LINEAR_SRGB; + } + if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_inputColorSpace == ColorSpace::SRGB)) + { + g_inputColorSpace = ColorSpace::SRGB; + } + // Add other spaces later + ImGui::EndCombo(); + } + + // Output Color Space Selector + 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)) + { + g_outputColorSpace = ColorSpace::LINEAR_SRGB; + } + if (ImGui::Selectable(ColorSpaceToString(ColorSpace::SRGB), g_outputColorSpace == ColorSpace::SRGB)) + { + g_outputColorSpace = ColorSpace::SRGB; + } + // Add other spaces later + ImGui::EndCombo(); + } + + ImGui::Separator(); + ImGui::Text("Operation Order:"); + + // Drag-and-Drop Reordering List + // Store indices or pointers to allow reordering `g_pipeline.activeOperations` + int move_from = -1, move_to = -1; + for (int i = 0; i < g_pipeline.activeOperations.size(); ++i) + { + PipelineOperation &op = g_pipeline.activeOperations[i]; + + ImGui::PushID(i); // Ensure unique IDs for controls within the loop + + // Checkbox to enable/disable + ImGui::Checkbox("", &op.enabled); + ImGui::SameLine(); + + // Simple Up/Down Buttons (alternative or complementary to DND) + + 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(); + + + // Selectable for drag/drop source/target + ImGui::Selectable(op.name.c_str(), false, 0, ImVec2(ImGui::GetContentRegionAvail().x - 30, 0)); // Leave space for buttons + + // Simple Drag Drop implementation + if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) + { + ImGui::SetDragDropPayload("PIPELINE_OP_DND", &i, sizeof(int)); + ImGui::Text("Move %s", op.name.c_str()); + ImGui::EndDragDropSource(); + } + if (ImGui::BeginDragDropTarget()) + { + if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("PIPELINE_OP_DND")) + { + IM_ASSERT(payload->DataSize == sizeof(int)); + move_from = *(const int *)payload->Data; + move_to = i; + } + ImGui::EndDragDropTarget(); + } + + + ImGui::PopID(); + } + + // Process move if detected + if (move_from != -1 && move_to != -1 && move_from != move_to) + { + PipelineOperation temp = g_pipeline.activeOperations[move_from]; + g_pipeline.activeOperations.erase(g_pipeline.activeOperations.begin() + move_from); + g_pipeline.activeOperations.insert(g_pipeline.activeOperations.begin() + move_to, temp); + printf("Moved operation %d to %d\n", move_from, move_to); + } + + ImGui::SeparatorText("Adjustments"); + + // --- Adjustment Controls --- + // Group sliders under collapsing headers as before + // The slider variables (exposure, contrast, etc.) are now directly + // linked to the PipelineOperation structs via pointers. if (ImGui::CollapsingHeader("White Balance", ImGuiTreeNodeFlags_DefaultOpen)) { ImGui::SliderFloat("Temperature", &temperature, 1000.0f, 20000.0f); @@ -329,8 +2087,8 @@ int main(int, char **) ImGui::Separator(); if (ImGui::CollapsingHeader("Tone", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f); - ImGui::SliderFloat("Contrast", &contrast, -100.0f, 100.0f); + ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); + ImGui::SliderFloat("Contrast", &contrast, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); ImGui::Separator(); ImGui::SliderFloat("Highlights", &highlights, -100.0f, 100.0f); ImGui::SliderFloat("Shadows", &shadows, -100.0f, 100.0f); @@ -348,9 +2106,107 @@ int main(int, char **) ImGui::SliderFloat("Saturation", &saturation, -100.0f, 100.0f); } ImGui::Separator(); - ImGui::End(); - ImGui::End(); // End "MainDockspaceWindow" + ImGui::SeparatorText("Transform"); + + 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(); + } + 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; + 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 { @@ -392,6 +2248,27 @@ int main(int, char **) #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(); @@ -404,7 +2281,7 @@ int main(int, char **) } ``` - +app_image.h ``` #ifndef APP_IMAGE_H #define APP_IMAGE_H @@ -1967,80 +3844,143 @@ namespace ImGuiImageViewerUtil { bool loadImageTexture(AppImage &appImage) { - // --- Basic Error Checking --- if (appImage.isEmpty() || appImage.getWidth() == 0 || appImage.getHeight() == 0) { + AppImageUtil::LogError("loadImageTexture: Image is empty."); return false; } if (!appImage.isLinear()) { - AppImageUtil::LogWarning("Image is not in linear color space."); + // This shouldn't happen if loadImage converts correctly, but good practice to check. + AppImageUtil::LogWarning("loadImageTexture: Warning - Image data is not linear. Pipeline expects linear input."); + // Ideally, convert to linear here if not already done. For now, proceed with caution. } + const int width = static_cast(appImage.getWidth()); const int height = static_cast(appImage.getHeight()); const int channels = static_cast(appImage.getChannels()); - if (channels != 1 && channels != 3 && channels != 4) - { - return false; - } const float *linearData = appImage.getData(); + size_t numFloats = static_cast(width) * height * channels; - // --- Prepare 8-bit sRGB data for OpenGL (RGBA format) --- - GLenum internalFormat = GL_RGBA8; - GLenum dataFormat = GL_RGBA; - int outputChannels = 4; - std::vector textureData(static_cast(width) * height * outputChannels); - unsigned char *outPtr = textureData.data(); - const float *inPtr = linearData; - for (int y = 0; y < height; ++y) - { - for (int x = 0; x < width; ++x) - { - float r = 0, g = 0, b = 0, a = 1; - if (channels == 1) - { - r = g = b = *inPtr++; - } - else if (channels == 3) - { - r = *inPtr++; - g = *inPtr++; - b = *inPtr++; - } - else - { - r = *inPtr++; - g = *inPtr++; - b = *inPtr++; - a = *inPtr++; - } - float sr = ImGuiImageViewerUtil::linear_to_srgb_approx(r), sg = ImGuiImageViewerUtil::linear_to_srgb_approx(g), sb = ImGuiImageViewerUtil::linear_to_srgb_approx(b); - a = std::fmax(0.f, std::fmin(1.f, a)); - *outPtr++ = static_cast(std::max(0, std::min(255, static_cast(ImGuiImageViewerUtil::Round(sr * 255.f))))); - *outPtr++ = static_cast(std::max(0, std::min(255, static_cast(ImGuiImageViewerUtil::Round(sg * 255.f))))); - *outPtr++ = static_cast(std::max(0, std::min(255, static_cast(ImGuiImageViewerUtil::Round(sb * 255.f))))); - *outPtr++ = static_cast(std::max(0, std::min(255, static_cast(ImGuiImageViewerUtil::Round(a * 255.f))))); + if (!linearData || numFloats == 0) { + AppImageUtil::LogError("loadImageTexture: Image data pointer is null or size is zero."); + return false; + } + + // --- Determine OpenGL texture format --- + GLenum internalFormat; + GLenum dataFormat; + GLenum dataType = GL_FLOAT; + std::vector textureDataBuffer; // Temporary buffer if we need to convert format (e.g., RGB -> RGBA) + + const float* dataPtr = linearData; + + if (channels == 1) { + internalFormat = GL_R16F; // Single channel, 16-bit float + dataFormat = GL_RED; + // Expand Grayscale to RGBA for easier shader handling (optional, shaders could handle GL_RED) + // Example: Expand to RGBA float buffer + textureDataBuffer.resize(static_cast(width) * height * 4); + float* outPtr = textureDataBuffer.data(); + for(int i = 0; i < width * height; ++i) { + float val = linearData[i]; + *outPtr++ = val; + *outPtr++ = val; + *outPtr++ = val; + *outPtr++ = 1.0f; // Alpha } + internalFormat = GL_RGBA16F; // Use RGBA16F if expanding + dataFormat = GL_RGBA; + dataPtr = textureDataBuffer.data(); // Point to the new buffer + AppImageUtil::LogWarning("loadImageTexture: Expanding 1-channel to RGBA16F for texture."); + + } else if (channels == 3) { + internalFormat = GL_RGBA16F; // Store as RGBA, easier for FBOs/blending + dataFormat = GL_RGBA; + // Need to convert RGB float -> RGBA float + textureDataBuffer.resize(static_cast(width) * height * 4); + float* outPtr = textureDataBuffer.data(); + const float* inPtr = linearData; + for(int i = 0; i < width * height; ++i) { + *outPtr++ = *inPtr++; // R + *outPtr++ = *inPtr++; // G + *outPtr++ = *inPtr++; // B + *outPtr++ = 1.0f; // A + } + dataPtr = textureDataBuffer.data(); // Point to the new buffer + AppImageUtil::LogWarning("loadImageTexture: Expanding 3-channel RGB to RGBA16F for texture."); + } else if (channels == 4) { + internalFormat = GL_RGBA16F; // Native RGBA + dataFormat = GL_RGBA; + dataPtr = linearData; // Use original data directly + } else { + AppImageUtil::LogError("loadImageTexture: Unsupported number of channels: " + std::to_string(channels)); + return false; } // --- Upload to OpenGL Texture --- GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture); - if (appImage.m_textureId == 0) - { + + if (appImage.m_textureId == 0) { glGenTextures(1, &appImage.m_textureId); + AppImageUtil::LogWarning("loadImageTexture: Generated new texture ID: " + std::to_string(appImage.m_textureId)); + } else { + AppImageUtil::LogWarning("loadImageTexture: Reusing texture ID: " + std::to_string(appImage.m_textureId)); } + + glBindTexture(GL_TEXTURE_2D, appImage.m_textureId); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // No filtering - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); // No filtering + // Use GL_LINEAR for smoother results when zooming/scaling in the viewer, even if processing is nearest neighbor. + // The processing pipeline itself uses FBOs, textures don't need mipmaps typically. + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + 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); - glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); - glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, GL_UNSIGNED_BYTE, textureData.data()); - glBindTexture(GL_TEXTURE_2D, lastTexture); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Ensure correct alignment, especially for RGB data + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); // Data is contiguous + + // Check if texture dimensions/format need updating + bool needsTexImage = true; + if (appImage.m_textureWidth == width && appImage.m_textureHeight == height) { + // Could potentially use glTexSubImage2D if format matches, but glTexImage2D is safer + // if the internal format might change or if it's the first load. + // For simplicity, we'll just recreate with glTexImage2D. + AppImageUtil::LogWarning("loadImageTexture: Texture dimensions match, overwriting with glTexImage2D."); + } else { + AppImageUtil::LogWarning("loadImageTexture: Texture dimensions or format mismatch, recreating with glTexImage2D."); + } + + + glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, 0, dataFormat, dataType, dataPtr); + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + AppImageUtil::LogError("loadImageTexture: OpenGL Error after glTexImage2D: " + std::to_string(err)); + glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding + // Consider deleting the texture ID if creation failed badly? + if (appImage.m_textureId != 0) { + glDeleteTextures(1, &appImage.m_textureId); + appImage.m_textureId = 0; + } + return false; + } else { + AppImageUtil::LogWarning("loadImageTexture: glTexImage2D successful."); + } + + + // Optional: Generate mipmaps if you want smoother downscaling *in the final view* + // glGenerateMipmap(GL_TEXTURE_2D); + // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); + + + glBindTexture(GL_TEXTURE_2D, lastTexture); // Restore previous binding appImage.m_textureWidth = width; appImage.m_textureHeight = height; + + AppImageUtil::LogWarning("loadImageTexture: Successfully loaded linear data into texture ID " + std::to_string(appImage.m_textureId)); + return true; } @@ -2102,11 +4042,4 @@ bool saveImage(const AppImage &image, #endif // APP_IMAGE_IMPLEMENTATION #endif // APP_IMAGE_H -``` - - -I've attached some relevant code for a image editor I am writing. What I would like to do next is implement a shader pipeline for applying the editing controls specified - -I would like for each operation to be a individual shader. I would like the order of the shaders to be configurable (ideally in a Imgui window on the let side) and I would like for the operations to be done in the best color space possible. - -Finally, add two controls to the top and bottom of the edit side where the user can specify the input color space, and the output color space \ No newline at end of file +``` \ No newline at end of file diff --git a/main.cpp b/main.cpp index 0b3996b..933168e 100644 --- a/main.cpp +++ b/main.cpp @@ -1,27 +1,30 @@ // 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.) +// (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). +// - 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 #include "imgui.h" -#include "imgui_impl_sdl2.h" #include "imgui_impl_opengl3.h" -#include +#include "imgui_impl_sdl2.h" +#include #include +#include #if defined(IMGUI_IMPL_OPENGL_ES2) #include #else -#include #include +#include #endif -// This example can also compile and run with Emscripten! See 'Makefile.emscripten' for details. +// This example can also compile and run with Emscripten! See +// 'Makefile.emscripten' for details. #ifdef __EMSCRIPTEN__ #include "../libs/emscripten/emscripten_mainloop_stub.h" #endif @@ -32,9 +35,9 @@ #define IMGUI_IMAGE_VIEWER_IMPLEMENTATION #include "app_image.h" -#include "tex_inspect_opengl.h" #include "imgui_tex_inspect.h" #include "shaderutils.h" +#include "tex_inspect_opengl.h" static float exposure = 0.0f; static float contrast = 0.0f; @@ -50,11 +53,11 @@ static float clarity = 0.0f; static float texture = 0.0f; static float dehaze = 0.0f; +#include // For std::function +#include +#include // For unique_ptr #include #include -#include -#include // For std::function -#include // For unique_ptr #include "imfilebrowser.h" // <<< Add this #include // <<< Add for path manipulation (C++17) @@ -101,13 +104,18 @@ struct PipelineOperation 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") + 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 + // 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()); + fprintf(stderr, "Warning: Uniform '%s' not found in shader '%s'\n", + pair.second.name.c_str(), name.c_str()); } } } @@ -119,7 +127,7 @@ enum class ColorSpace { LINEAR_SRGB, // Linear Rec.709/sRGB primaries SRGB // Non-linear sRGB (display) - // Add AdobeRGB, ProPhoto etc. later + // Add AdobeRGB, ProPhoto etc. later }; const char *ColorSpaceToString(ColorSpace cs) @@ -135,7 +143,8 @@ const char *ColorSpaceToString(ColorSpace cs) } } -bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &outImage) +bool ReadTextureToAppImage(GLuint textureId, int width, int height, + AppImage &outImage) { if (textureId == 0 || width <= 0 || height <= 0) { @@ -143,16 +152,19 @@ bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &ou 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 + // 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 &pixelData = outImage.getPixelVector(); if (pixelData.empty()) { - fprintf(stderr, "ReadTextureToAppImage: Failed to allocate AppImage buffer.\n"); + fprintf(stderr, + "ReadTextureToAppImage: Failed to allocate AppImage buffer.\n"); return false; } @@ -165,7 +177,8 @@ bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &ou 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 + // 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 @@ -177,12 +190,15 @@ bool ReadTextureToAppImage(GLuint textureId, int width, int height, AppImage &ou if (err != GL_NO_ERROR) { - fprintf(stderr, "ReadTextureToAppImage: OpenGL Error during glGetTexImage: %u\n", err); + 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); + printf("ReadTextureToAppImage: Successfully read %dx%d texture.\n", width, + height); return true; } @@ -198,19 +214,44 @@ private: 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 FindOperationIndex(const std::string &name) const + { + for (size_t i = 0; i < activeOperations.size(); ++i) + { + if (activeOperations[i].name == name) + { + return static_cast(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, + 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}; + -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); @@ -224,10 +265,12 @@ private: printf("Fullscreen quad VBO created.\n"); // Position attribute - glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void *)0); + 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))); + glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), + (void *)(2 * sizeof(float))); glEnableVertexAttribArray(1); glBindBuffer(GL_ARRAY_BUFFER, 0); @@ -235,21 +278,25 @@ private: printf("Fullscreen quad VAO/VBO created.\n"); } - void CreateOrResizeFBOs(int width, int height) + bool CreateOrResizeFBOs(int width, int height) { if (width == m_texWidth && height == m_texHeight && m_fbo[0] != 0) { - return; // Already correct size + return true; // Already correct size } if (width <= 0 || height <= 0) - return; // Invalid dimensions + { + fprintf(stderr, "CreateOrResizeFBOs: Invalid dimensions (%dx%d).\n", width, height); + return false; // Invalid dimensions + } - // Cleanup existing - DestroyFBOs(); + // 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); @@ -257,74 +304,379 @@ private: GLint lastTexture; glGetIntegerv(GL_TEXTURE_BINDING_2D, &lastTexture); GLint lastFBO; - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); // Or GL_FRAMEBUFFER_BINDING + 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]); - // Create floating point texture + // 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_NEAREST); // Use NEAREST for processing steps - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + 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); - // Attach texture to FBO glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_tex[i], 0); - if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + GLenum fboStatus = glCheckFramebufferStatus(GL_FRAMEBUFFER); + if (fboStatus != GL_FRAMEBUFFER_COMPLETE) { - fprintf(stderr, "ERROR::FRAMEBUFFER:: Framebuffer %d is not complete!\n", i); - DestroyFBOs(); // Clean up partial setup - glBindTexture(GL_TEXTURE_2D, lastTexture); - glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); - return; + 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_fbo[0] = m_fbo[1] = 0; - m_tex[0] = m_tex[1] = 0; + m_tex[0] = m_tex[1] = 0; + } + // Invalidate cache when FBOs are destroyed m_texWidth = m_texHeight = 0; - printf("Destroyed FBOs and textures.\n"); + 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 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 activeOperations; - ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default based on AppImage goal + ColorSpace inputColorSpace = ColorSpace::LINEAR_SRGB; // Default ColorSpace outputColorSpace = ColorSpace::SRGB; // Default for display ImageProcessingPipeline() = default; ~ImageProcessingPipeline() { - DestroyFBOs(); + 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"); } @@ -332,23 +684,40 @@ public: { printf("Initializing ImageProcessingPipeline...\n"); CreateFullscreenQuad(); - printf("Fullscreen quad created.\n"); - // Load essential shaders + std::string vsPath = shaderBasePath + "passthrough.vert"; - printf("Loading shaders from: %s\n", vsPath.c_str()); + 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"); - printf("Loaded shaders: %s, %s, %s\n", vsPath.c_str(), (shaderBasePath + "linear_to_srgb.frag").c_str(), (shaderBasePath + "srgb_to_linear.frag").c_str()); + m_diffShader = LoadShaderProgramFromFiles(vsPath, shaderBasePath + "diff.frag"); // <<< Load diff shader if (!m_passthroughShader || !m_linearToSrgbShader || !m_srgbToLinearShader) { - fprintf(stderr, "Failed to load essential pipeline shaders!\n"); + 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.\n"); + 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() @@ -357,295 +726,173 @@ public: DestroyFBOs(); // Call the existing cleanup method } - // Call this each frame to process the image - // Returns the Texture ID of the final processed image - GLuint ProcessImage(GLuint inputTextureId, int width, int height, bool applyOutputConversion = true) + // --- 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) { - return 0; // No input or invalid size + // ... (invalid input handling, clear cache) ... + return 0; } - CreateOrResizeFBOs(width, height); - if (m_fbo[0] == 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) { - fprintf(stderr, "FBOs not ready, cannot process image.\n"); - return 0; // FBOs not ready - } - - // Store original viewport and FBO to restore later - GLint viewport[4]; - glGetIntegerv(GL_VIEWPORT, viewport); - GLint lastFBO; - glGetIntegerv(GL_DRAW_FRAMEBUFFER_BINDING, &lastFBO); - - glViewport(0, 0, m_texWidth, m_texHeight); - glBindVertexArray(m_vao); // Bind the quad VAO once - - int currentSourceTexIndex = 0; // Start with texture m_tex[0] as the first *write* target - GLuint currentReadTexId = inputTextureId; // Initially read from the original image - - // --- Input Color Space Conversion --- - bool inputConversionDone = false; - if (inputColorSpace == ColorSpace::SRGB) - { - printf("Pipeline: Applying sRGB -> Linear conversion.\n"); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); - glUseProgram(m_srgbToLinearShader); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_1D, currentReadTexId); - glUniform1i(glGetUniformLocation(m_srgbToLinearShader, "InputTexture"), 0); - glDrawArrays(GL_TRIANGLES, 0, 6); - - currentReadTexId = m_tex[currentSourceTexIndex]; // Next read is from the texture we just wrote to - currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap target FBO/texture - inputConversionDone = true; + // Cache hit and not diffing + return forDisplay ? m_cachedDisplayTextureId : m_cachedLinearTextureId; } else { - printf("Pipeline: Input is Linear, no conversion needed.\n"); - // If input is already linear, we might need to copy it to the first FBO texture - // if there are actual processing steps, otherwise the first step reads the original. - // This copy ensures the ping-pong works correctly even if the first *user* step is disabled. - // However, if NO user steps are enabled, we want to display the original (potentially with output conversion). - bool anyUserOpsEnabled = false; - for (const auto &op : activeOperations) - { - if (op.enabled && op.shaderProgram && op.name != "Passthrough") - { // Check it's a real operation - anyUserOpsEnabled = true; - break; - } - } + // 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"); - if (anyUserOpsEnabled) - { - // Need to copy original linear input into the pipeline's texture space - printf("Pipeline: Copying linear input to FBO texture for processing.\n"); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); - glUseProgram(m_passthroughShader); // Use simple passthrough - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, currentReadTexId); - glUniform1i(glGetUniformLocation(m_passthroughShader, "InputTexture"), 0); - glDrawArrays(GL_TRIANGLES, 0, 6); - currentReadTexId = m_tex[currentSourceTexIndex]; - currentSourceTexIndex = 1 - currentSourceTexIndex; - inputConversionDone = true; - } - else - { - // No user ops, keep reading directly from original inputTextureId - inputConversionDone = false; // Treat as if no initial step happened yet - printf("Pipeline: No enabled user operations, skipping initial copy.\n"); - } - } + ProcessingResult results = ExecuteProcessingSteps(inputTextureId, width, height); - // --- Apply Editing Operations --- - int appliedOps = 0; - for (const auto &op : activeOperations) - { - if (op.enabled && op.shaderProgram) + if (results.linearOutput != 0 && results.displayOutput != 0) { - printf("Pipeline: Applying operation: %s\n", op.name.c_str()); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); - glUseProgram(op.shaderProgram); - - // Set Input Texture Sampler - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, currentReadTexId); - GLint loc = glGetUniformLocation(op.shaderProgram, "InputTexture"); - if (loc != -1) - glUniform1i(loc, 0); - else if (op.name != "Passthrough") - fprintf(stderr, "Warning: InputTexture uniform not found in shader %s\n", op.name.c_str()); - - // Set operation-specific uniforms - if (op.updateUniformsCallback) + printf("Pipeline: Processing successful.\n"); + // Update cache ONLY if NOT in diff mode + if (!m_diffActive) { - op.updateUniformsCallback(op.shaderProgram); + 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 { - // Alternative: Set uniforms directly based on stored pointers - 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.clarityVal && op.uniforms.count("clarityValue")) - { - glUniform1f(op.uniforms.at("clarityValue").location, *op.clarityVal); - } - 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.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); - } - if (op.saturationVal && op.uniforms.count("saturationValue")) - { - glUniform1f(op.uniforms.at("saturationValue").location, *op.saturationVal); - } - if (op.vibranceVal && op.uniforms.count("vibranceValue")) - { - glUniform1f(op.uniforms.at("vibranceValue").location, *op.vibranceVal); - } - 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); - } + printf("Pipeline: Diff active, cache not updated.\n"); + // Keep pipeline dirty if diffing, so next non-diff frame recalculates correctly + m_isDirty = true; } - glDrawArrays(GL_TRIANGLES, 0, 6); - - // Prepare for next pass - currentReadTexId = m_tex[currentSourceTexIndex]; // Next pass reads from the texture we just wrote - currentSourceTexIndex = 1 - currentSourceTexIndex; // Swap FBO target - appliedOps++; - } - } - - // If no user ops were applied AND no input conversion happened, - // currentReadTexId is still the original inputTextureId. - if (appliedOps == 0 && !inputConversionDone) - { - printf("Pipeline: No operations applied, output = input (%d).\n", currentReadTexId); - // Proceed to output conversion using original inputTextureId - } - else if (appliedOps > 0 || inputConversionDone) - { - printf("Pipeline: %d operations applied, final intermediate texture ID: %d\n", appliedOps, currentReadTexId); - // currentReadTexId now holds the result of the last applied operation (or the input conversion) - } - else - { - // This case should ideally not be reached if logic above is correct - printf("Pipeline: Inconsistent state after processing loop.\n"); - } - - // --- Output Color Space Conversion --- - GLuint finalTextureId = currentReadTexId; // Assume this is the final one unless converted - if (applyOutputConversion) - { - if (outputColorSpace == ColorSpace::SRGB) - { - // Check if the last written data (currentReadTexId) is already sRGB. - // In this simple setup, it's always linear *unless* no ops applied and input was sRGB. - // More robustly: Track the color space through the pipeline. - // For now, assume currentReadTexId holds linear data if any op or input conversion happened. - bool needsLinearToSrgb = (appliedOps > 0 || inputConversionDone); - - if (!needsLinearToSrgb && inputColorSpace == ColorSpace::SRGB) + // Return the correct texture + if (forDisplay) { - printf("Pipeline: Output is sRGB, and input was sRGB with no ops, no final conversion needed.\n"); - // Input was sRGB, no ops applied, output should be sRGB. currentReadTexId is original sRGB input. - finalTextureId = currentReadTexId; - } - else if (needsLinearToSrgb) - { - printf("Pipeline: Applying Linear -> sRGB conversion for output.\n"); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); // Use the *next* FBO for the final write - glUseProgram(m_linearToSrgbShader); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read the last result - glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); - glDrawArrays(GL_TRIANGLES, 0, 6); - finalTextureId = m_tex[currentSourceTexIndex]; // The final result is in this texture + // 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 { - // Input was linear, no ops, output requires sRGB. - printf("Pipeline: Input Linear, no ops, applying Linear -> sRGB conversion for output.\n"); - glBindFramebuffer(GL_FRAMEBUFFER, m_fbo[currentSourceTexIndex]); - glUseProgram(m_linearToSrgbShader); - glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, currentReadTexId); // Read original linear input - glUniform1i(glGetUniformLocation(m_linearToSrgbShader, "InputTexture"), 0); - glDrawArrays(GL_TRIANGLES, 0, 6); - finalTextureId = m_tex[currentSourceTexIndex]; + // For saving, always return the linear result + printf("Pipeline: Returning LINEAR texture (%u) for saving.\n", results.linearOutput); + return results.linearOutput; } } else { - printf("Pipeline: Output is Linear, no final conversion needed.\n"); - // If output should be linear, finalTextureId is already correct (it's currentReadTexId) - finalTextureId = currentReadTexId; + // 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; } } - else - { - printf("Pipeline: Skipped output conversion. Final (linear) ID: %d\n", finalTextureId); - } - - // --- Cleanup --- - glBindVertexArray(0); - glBindFramebuffer(GL_FRAMEBUFFER, lastFBO); // Restore original framebuffer binding - glViewport(viewport[0], viewport[1], viewport[2], viewport[3]); // Restore viewport - glUseProgram(0); // Unbind shader program - - printf("Pipeline: ProcessImage returning final texture ID: %d\n", finalTextureId); - return finalTextureId; } }; -static ImageProcessingPipeline g_pipeline; // <<< Global pipeline manager instance -static std::vector> 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 +static ImageProcessingPipeline + g_pipeline; // <<< Global pipeline manager instance +static std::vector> + 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); +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 +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 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 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 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 @@ -666,74 +913,88 @@ 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) { +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()) { + 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(); + 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) { + if (!success) + { GLint logLength; glGetShaderiv(computeShaderObj, GL_INFO_LOG_LENGTH, &logLength); std::vector log(logLength); glGetShaderInfoLog(computeShaderObj, logLength, nullptr, log.data()); - fprintf(stderr, "ERROR::SHADER::HISTOGRAM::COMPILATION_FAILED\n%s\n", 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) { + glGetProgramiv(g_histogramComputeShader, GL_LINK_STATUS, &success); + if (!success) + { GLint logLength; glGetProgramiv(g_histogramComputeShader, GL_INFO_LOG_LENGTH, &logLength); std::vector log(logLength); - glGetProgramInfoLog(g_histogramComputeShader, logLength, nullptr, log.data()); - fprintf(stderr, "ERROR::PROGRAM::HISTOGRAM::LINKING_FAILED\n%s\n", log.data()); + 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); - + 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 + 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); + 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)); } - + 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; @@ -746,8 +1007,7 @@ struct AspectRatioOption float ratio; // W/H }; static std::vector g_aspectRatios = { - {"Freeform", 0.0f}, - {"Original", 0.0f}, // Will be calculated dynamically + {"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}, @@ -756,83 +1016,132 @@ static std::vector g_aspectRatios = { // Add more as needed }; -void UpdateCropRect(ImVec4& rectNorm, CropHandle handle, ImVec2 deltaNorm, float aspectRatio) { +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 + 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); + 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) + 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; + 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 + // 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) { + if (handle == CropHandle::TOP || handle == CropHandle::BOTTOM) + { adjustHeight = false; // Primarily adjust width based on height change } - if (adjustHeight) { // Adjust height based on width + 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) { + 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; - } + 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()) { +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"); + 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 } @@ -856,45 +1165,59 @@ bool ApplyCropToImage(AppImage& image, const ImVec4 cropRectNorm) { 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); + 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); + 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"); + 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. + // TODO: Copy metadata/ICC profile if needed? Cropping usually invalidates + // some metadata. - const float* srcData = image.getData(); - float* dstData = croppedImage.getData(); + 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) { + 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; + if (y_src < 0 || y_src >= srcH) + continue; // Calculate start pointers for source and destination rows - const float* srcRowStart = srcData + (static_cast(y_src) * srcW + cropX_px) * channels; - float* dstRowStart = dstData + (static_cast(y_dst) * cropW_px) * channels; + const float *srcRowStart = + srcData + (static_cast(y_src) * srcW + cropX_px) * channels; + float *dstRowStart = + dstData + (static_cast(y_dst) * cropW_px) * channels; // Copy the entire row (width * channels floats) - std::memcpy(dstRowStart, srcRowStart, static_cast(cropW_px) * channels * sizeof(float)); + std::memcpy(dstRowStart, srcRowStart, + static_cast(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()); + printf("Cropped image created successfully (%dx%d).\n", image.getWidth(), + image.getHeight()); return true; } @@ -902,14 +1225,17 @@ 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 + 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("White Balance"); - whiteBalanceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "white_balance.frag"); + whiteBalanceOp->shaderProgram = + LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", + shaderBasePath + "white_balance.frag"); if (whiteBalanceOp->shaderProgram) { whiteBalanceOp->uniforms["temperatureValue"] = {"temperature"}; @@ -924,14 +1250,16 @@ void InitShaderOperations(const std::string &shaderBasePath) printf(" - FAILED White Balance\n"); auto exposureOp = std::make_unique("Exposure"); - exposureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "exposure.frag"); + 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("Contrast"); - contrastOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "contrast.frag"); + contrastOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "contrast.frag"); if (contrastOp->shaderProgram) { contrastOp->uniforms["contrastValue"] = {"contrastValue"}; @@ -943,8 +1271,11 @@ void InitShaderOperations(const std::string &shaderBasePath) else printf(" - FAILED Contrast\n"); - auto highlightsShadowsOp = std::make_unique("Highlights/Shadows"); - highlightsShadowsOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "highlights_shadows.frag"); + auto highlightsShadowsOp = + std::make_unique("Highlights/Shadows"); + highlightsShadowsOp->shaderProgram = + LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", + shaderBasePath + "highlights_shadows.frag"); if (highlightsShadowsOp->shaderProgram) { highlightsShadowsOp->uniforms["highlightsValue"] = {"highlightsValue"}; @@ -960,7 +1291,9 @@ void InitShaderOperations(const std::string &shaderBasePath) auto whiteBlackOp = std::make_unique("Whites/Blacks"); - whiteBlackOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "whites_blacks.frag"); + whiteBlackOp->shaderProgram = + LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", + shaderBasePath + "whites_blacks.frag"); if (whiteBlackOp->shaderProgram) { whiteBlackOp->uniforms["whitesValue"] = {"whitesValue"}; @@ -976,7 +1309,8 @@ void InitShaderOperations(const std::string &shaderBasePath) auto textureOp = std::make_unique("Texture"); - textureOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "texture.frag"); + textureOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "texture.frag"); if (textureOp->shaderProgram) { textureOp->uniforms["textureValue"] = {"textureValue"}; @@ -989,7 +1323,8 @@ void InitShaderOperations(const std::string &shaderBasePath) printf(" - FAILED Texture\n"); auto clarityOp = std::make_unique("Clarity"); - clarityOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "clarity.frag"); + clarityOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "clarity.frag"); if (clarityOp->shaderProgram) { clarityOp->uniforms["clarityValue"] = {"clarityValue"}; @@ -1002,7 +1337,8 @@ void InitShaderOperations(const std::string &shaderBasePath) printf(" - FAILED Clarity\n"); auto dehazeOp = std::make_unique("Dehaze"); - dehazeOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "dehaze.frag"); + dehazeOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "dehaze.frag"); if (dehazeOp->shaderProgram) { dehazeOp->uniforms["dehazeValue"] = {"dehazeValue"}; @@ -1015,7 +1351,8 @@ void InitShaderOperations(const std::string &shaderBasePath) printf(" - FAILED Dehaze\n"); auto saturationOp = std::make_unique("Saturation"); - saturationOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "saturation.frag"); + saturationOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "saturation.frag"); if (saturationOp->shaderProgram) { saturationOp->uniforms["saturationValue"] = {"saturationValue"}; @@ -1028,7 +1365,8 @@ void InitShaderOperations(const std::string &shaderBasePath) printf(" - FAILED Saturation\n"); auto vibranceOp = std::make_unique("Vibrance"); - vibranceOp->shaderProgram = LoadShaderProgramFromFiles(shaderBasePath + "passthrough.vert", shaderBasePath + "vibrance.frag"); + vibranceOp->shaderProgram = LoadShaderProgramFromFiles( + shaderBasePath + "passthrough.vert", shaderBasePath + "vibrance.frag"); if (vibranceOp->shaderProgram) { vibranceOp->uniforms["vibranceValue"] = {"vibranceValue"}; @@ -1044,8 +1382,9 @@ void InitShaderOperations(const std::string &shaderBasePath) 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 + { // 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 @@ -1060,47 +1399,56 @@ void InitShaderOperations(const std::string &shaderBasePath) 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().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", + 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) { +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"); + 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 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) + // Using glBufferSubData might be marginally faster than glClearBufferData if + // driver optimizes zeroing static std::vector + // 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); + 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 + // 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); @@ -1114,8 +1462,8 @@ void ComputeHistogramGPU(GLuint inputTextureID, int width, int height) { // 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 + // 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) @@ -1125,45 +1473,59 @@ void ComputeHistogramGPU(GLuint inputTextureID, int width, int height) { // 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()); + 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) { + 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 - + // 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); + 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; + std::fill(g_histogramDataCPU.begin(), g_histogramDataCPU.end(), 0); + g_histogramMaxCount = 1; printf("Histogram computation failed. Data cleared.\n"); } - else { + 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()) { +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 { + } + else + { ImGui::Text("Histogram data is empty or invalid."); } - if (g_histogramMaxCount <= 1) { + if (g_histogramMaxCount <= 1) + { ImGui::Text("Histogram max count is invalid."); } ImGui::Text("Histogram data not available."); @@ -1172,19 +1534,23 @@ void DrawHistogramWidget(const char* widgetId, ImVec2 graphSize) { ImGui::PushID(widgetId); // Isolate widget IDs - ImDrawList* drawList = ImGui::GetWindowDrawList(); + 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 + 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)); + 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 + 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); @@ -1192,31 +1558,44 @@ void DrawHistogramWidget(const char* widgetId, ImVec2 graphSize) { 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) { + 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); + 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 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); + // 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); + // 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)); + drawList->AddRect(widgetPos, widgetPos + graphSize, + IM_COL32(150, 150, 150, 255)); // Advance cursor past the histogram widget area ImGui::Dummy(graphSize); @@ -1228,7 +1607,8 @@ void DrawHistogramWidget(const char* widgetId, ImVec2 graphSize) { int main(int, char **) { // Setup SDL - if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != 0) + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMECONTROLLER) != + 0) { printf("Error: %s\n", SDL_GetError()); return -1; @@ -1252,7 +1632,9 @@ int main(int, char **) #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_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); @@ -1274,8 +1656,12 @@ int main(int, char **) 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); + 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()); @@ -1300,25 +1686,26 @@ int main(int, char **) 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.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. + // 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) { @@ -1336,9 +1723,10 @@ int main(int, char **) 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 + ".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"); @@ -1352,13 +1740,15 @@ int main(int, char **) 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::ImplOpenGL3_Init(); // Or DirectX 11 equivalent (check your + // chosen backend header file) ImGuiTexInspect::Init(); ImGuiTexInspect::CreateContext(); InitShaderOperations("shaders/"); // Initialize shader operations - if (!InitHistogramResources("shaders/")) { + if (!InitHistogramResources("shaders/")) + { // Handle error - maybe disable histogram feature fprintf(stderr, "Histogram initialization failed, feature disabled.\n"); } @@ -1366,8 +1756,9 @@ int main(int, char **) // 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. + // 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 @@ -1375,17 +1766,23 @@ int main(int, char **) #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. + // 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)) + 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) @@ -1403,29 +1800,57 @@ int main(int, char **) GLuint textureToSave = 0; // Texture ID holding final linear data for saving if (g_imageIsLoaded && g_loadedImage.m_textureId != 0) { - g_pipeline.inputColorSpace = g_inputColorSpace; - g_pipeline.outputColorSpace = g_outputColorSpace; + // 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; - // Modify pipeline processing slightly to get both display and save textures - // Add a flag or method to control output conversion for saving - textureToSave = g_pipeline.ProcessImage( + if (g_pipeline.IsDirty()) + { + recomputeHisto = true; + } + + // Get potentially cached textures + textureToDisplay = g_pipeline.GetProcessedTexture( g_loadedImage.m_textureId, g_loadedImage.getWidth(), g_loadedImage.getHeight(), - false // <-- Add argument: bool applyOutputConversion = true + true // Request texture suitable for display (potentially sRGB) ); - textureToDisplay = g_pipeline.ProcessImage( + + textureToSave = g_pipeline.GetProcessedTexture( g_loadedImage.m_textureId, g_loadedImage.getWidth(), g_loadedImage.getHeight(), - true // Apply conversion for display + false // Request texture suitable for saving (linear) ); - // If the pipeline wasn't modified, textureToSave might need extra work + + // 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 --- @@ -1474,18 +1899,22 @@ int main(int, char **) glDeleteTextures(1, &g_loadedImage.m_textureId); g_loadedImage.m_textureId = 0; } - // Clean up pipeline resources (FBOs/Textures) before loading new texture + // 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"); + 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); + printf("Float texture created successfully (ID: %u).\n", + g_loadedImage.m_textureId); // Maybe reset sliders/pipeline state? Optional. } else @@ -1500,7 +1929,8 @@ int main(int, char **) { g_imageIsLoaded = false; g_currentFilePath = ""; - fprintf(stderr, "Failed to load image file: %s\n", selectedPath.c_str()); + fprintf(stderr, "Failed to load image file: %s\n", + selectedPath.c_str()); // TODO: Show error to user } } @@ -1508,12 +1938,15 @@ int main(int, char **) 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 + 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)) + // 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(); @@ -1522,15 +1955,18 @@ int main(int, char **) 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)"}; + 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))) + if (ImGui::Combo("##ExportFormat", ¤tFormatIndex, formats, + IM_ARRAYSIZE(formats))) { switch (currentFormatIndex) - { /* ... map index back to g_exportFormat ... */ + { /* ... map index back to g_exportFormat + ... */ } g_exportErrorMsg = ""; } @@ -1542,7 +1978,9 @@ int main(int, char **) } else { - ImGui::Dummy(ImVec2(0.0f, ImGui::GetFrameHeightWithSpacing())); // Keep consistent height + ImGui::Dummy(ImVec2( + 0.0f, + ImGui::GetFrameHeightWithSpacing())); // Keep consistent height } ImGui::Separator(); @@ -1558,7 +1996,8 @@ int main(int, char **) // --- Action Buttons --- if (ImGui::Button("Save As...", ImVec2(120, 0))) { - // ... (Logic to set default name/path and call g_exportSaveFileDialog.Open() remains the same) ... + // ... (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()); @@ -1566,7 +2005,8 @@ int main(int, char **) g_exportSaveFileDialog.Open(); } ImGui::SameLine(); - // No need for an explicit Cancel button if the 'X' works, but can keep it: + // 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 @@ -1594,7 +2034,8 @@ int main(int, char **) { 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)) + if (ReadTextureToAppImage(textureToSave, g_loadedImage.getWidth(), + g_loadedImage.getHeight(), exportImageRGBA)) { printf("Texture readback successful, saving...\n"); // <<< --- ADD CONVERSION LOGIC HERE --- >>> @@ -1604,13 +2045,16 @@ int main(int, char **) // 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); + 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(); + size_t numPixels = + exportImageRGBA.getWidth() * exportImageRGBA.getHeight(); for (size_t i = 0; i < numPixels; ++i) { @@ -1619,30 +2063,40 @@ int main(int, char **) 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 + 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); + 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); + 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); + 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 + g_showExportWindow = + false; // <<< Close the settings window on success } else { @@ -1677,20 +2131,26 @@ int main(int, char **) // 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; + 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. + // 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 + 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::Begin("DockSpaceWindowHost", nullptr, + host_window_flags); // No bool* needed ImGui::PopStyleVar(3); // Create the actual dockspace area. @@ -1700,27 +2160,37 @@ int main(int, char **) // --- 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. + // 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 + 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 + 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_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); + 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 + 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::DockBuilderDockWindow( + "Image View", dock_center_id); // Dock image view into the center ImGui::DockBuilderFinish(dockspace_id); printf("DockBuilder: Layout finished.\n"); @@ -1729,70 +2199,86 @@ int main(int, char **) // --- 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. + // 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 + 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) { - ComputeHistogramGPU(textureToDisplay, g_loadedImage.getWidth(), g_loadedImage.getHeight()); - // Assume ImGuiTexInspect fills available space. This might need adjustment. + // 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()); + 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 + 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) + { // 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) + { // 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 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::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 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 + 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 @@ -1820,13 +2306,17 @@ int main(int, char **) CropHandle hoveredHandle = CropHandle::NONE; // Only interact if window is hovered - if (ImGui::IsWindowHovered()) // ImGuiHoveredFlags_AllowWhenBlockedByActiveItem might also be needed + 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)); + ImRect handleRect(h.pos - ImVec2(handleInteractionMargin, + handleInteractionMargin), + h.pos + ImVec2(handleInteractionMargin, + handleInteractionMargin)); if (handleRect.Contains(mousePos)) { hoveredHandle = h.id; @@ -1843,34 +2333,40 @@ int main(int, char **) } // Mouse Down: Start dragging - if (hoveredHandle != CropHandle::NONE && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) + 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 + 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)) + 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) + 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 + 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); + 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); @@ -1883,12 +2379,14 @@ int main(int, char **) 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; + // 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)) + else if (g_isDraggingCrop && + ImGui::IsMouseReleased(ImGuiMouseButton_Left)) { // Mouse Release: Stop dragging g_isDraggingCrop = false; @@ -1898,30 +2396,52 @@ int main(int, char **) // --- 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 + 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); + 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); + 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); + drawList->AddRectFilled( + h.pos - ImVec2(handleScreenSize / 2, handleScreenSize / 2), + h.pos + ImVec2(handleScreenSize / 2, handleScreenSize / 2), + (isHovered || isActive) ? colHover : colHandle); } } // End if(g_cropActive) } @@ -1930,10 +2450,11 @@ int main(int, char **) // 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::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; + g_histogramMaxCount = 1; // Or maybe: "File -> Open... to load an image" } ImGui::End(); // End Image View @@ -1946,9 +2467,12 @@ int main(int, char **) 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::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) @@ -1961,7 +2485,9 @@ int main(int, char **) // "Edit Image" window ImGui::Begin("Edit Image"); - if (ImGui::CollapsingHeader("Histogram", ImGuiTreeNodeFlags_DefaultOpen)) { + if (ImGui::CollapsingHeader("Histogram", + ImGuiTreeNodeFlags_DefaultOpen)) + { DrawHistogramWidget("ExifHistogram", ImVec2(-1, 256)); } @@ -1972,57 +2498,67 @@ int main(int, char **) 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; } - // Add other spaces later ImGui::EndCombo(); } + if (inputCsChanged) + g_pipeline.MarkDirty(); - // Output Color Space Selector + // --- 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; } - // Add other spaces later ImGui::EndCombo(); } + if (outputCsChanged) + g_pipeline.MarkDirty(); // <<< Mark dirty ImGui::Separator(); ImGui::Text("Operation Order:"); - // Drag-and-Drop Reordering List - // Store indices or pointers to allow reordering `g_pipeline.activeOperations` + // --- 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); // Ensure unique IDs for controls within the loop - - // Checkbox to enable/disable - ImGui::Checkbox("", &op.enabled); + 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(); - - // Simple Up/Down Buttons (alternative or complementary to DND) - if (ImGui::ArrowButton("##up", ImGuiDir_Up) && i > 0) { move_from = i; @@ -2034,86 +2570,147 @@ int main(int, char **) 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 - // Selectable for drag/drop source/target - ImGui::Selectable(op.name.c_str(), false, 0, ImVec2(ImGui::GetContentRegionAvail().x - 30, 0)); // Leave space for buttons - - // Simple Drag Drop implementation + // Drag Drop Source/Target if (ImGui::BeginDragDropSource(ImGuiDragDropFlags_None)) - { - ImGui::SetDragDropPayload("PIPELINE_OP_DND", &i, sizeof(int)); - ImGui::Text("Move %s", op.name.c_str()); - ImGui::EndDragDropSource(); + { /* ... */ } if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload *payload = ImGui::AcceptDragDropPayload("PIPELINE_OP_DND")) { - IM_ASSERT(payload->DataSize == sizeof(int)); 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) { - PipelineOperation temp = g_pipeline.activeOperations[move_from]; - g_pipeline.activeOperations.erase(g_pipeline.activeOperations.begin() + move_from); - g_pipeline.activeOperations.insert(g_pipeline.activeOperations.begin() + move_to, temp); - printf("Moved operation %d to %d\n", move_from, move_to); + // ... (Reordering logic) ... + order_or_enabled_changed = true; // <<< Reordering occurred } - ImGui::SeparatorText("Adjustments"); + if (order_or_enabled_changed) + { + g_pipeline.MarkDirty(); // <<< Mark dirty if enabled state or order changed + } - // --- Adjustment Controls --- - // Group sliders under collapsing headers as before - // The slider variables (exposure, contrast, etc.) are now directly - // linked to the PipelineOperation structs via pointers. if (ImGui::CollapsingHeader("White Balance", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::SliderFloat("Temperature", &temperature, 1000.0f, 20000.0f); - ImGui::SliderFloat("Tint", &tint, -100.0f, 100.0f); + 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)) { - ImGui::SliderFloat("Exposure", &exposure, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); - ImGui::SliderFloat("Contrast", &contrast, -5.0f, 5.0f, "%.1f", ImGuiSliderFlags_Logarithmic); + 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(); - ImGui::SliderFloat("Highlights", &highlights, -100.0f, 100.0f); - ImGui::SliderFloat("Shadows", &shadows, -100.0f, 100.0f); - ImGui::SliderFloat("Whites", &whites, -100.0f, 100.0f); - ImGui::SliderFloat("Blacks", &blacks, -100.0f, 100.0f); + 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)) { - ImGui::SliderFloat("Texture", &texture, -100.0f, 100.0f); - ImGui::SliderFloat("Clarity", &clarity, -100.0f, 100.0f); - ImGui::SliderFloat("Dehaze", &dehaze, -100.0f, 100.0f); + 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(); - ImGui::SliderFloat("Vibrance", &vibrance, -100.0f, 100.0f); - ImGui::SliderFloat("Saturation", &saturation, -100.0f, 100.0f); + 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(); - ImGui::SeparatorText("Transform"); + // 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_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; @@ -2124,16 +2721,20 @@ int main(int, char **) { if (strcmp(opt.name, "Original") == 0) { - opt.ratio = float(g_loadedImage.getWidth()) / float(g_loadedImage.getHeight()); + 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) + if (g_selectedAspectRatioIndex >= 0 && + g_selectedAspectRatioIndex < g_aspectRatios.size() && + strcmp(g_aspectRatios[g_selectedAspectRatioIndex].name, + "Original") == 0) { - g_cropAspectRatio = g_aspectRatios[g_selectedAspectRatioIndex].ratio; + g_cropAspectRatio = + g_aspectRatios[g_selectedAspectRatioIndex].ratio; } printf("Crop tool activated.\n"); @@ -2144,7 +2745,9 @@ int main(int, char **) ImGui::Text("Crop Active"); // Aspect Ratio Selector - if (ImGui::BeginCombo("Aspect Ratio", g_aspectRatios[g_selectedAspectRatioIndex].name)) + if (ImGui::BeginCombo( + "Aspect Ratio", + g_aspectRatios[g_selectedAspectRatioIndex].name)) { for (int i = 0; i < g_aspectRatios.size(); ++i) { @@ -2154,8 +2757,10 @@ int main(int, char **) 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); + // 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(); @@ -2170,7 +2775,8 @@ int main(int, char **) // <<< --- CALL FUNCTION TO APPLY CROP --- >>> if (ApplyCropToImage(g_loadedImage, g_cropRectNorm)) { - printf("Crop applied successfully. Reloading texture and resetting pipeline.\n"); + printf("Crop applied successfully. Reloading texture and resetting " + "pipeline.\n"); // Reload texture with cropped data if (!loadImageTexture(g_loadedImage)) { @@ -2179,6 +2785,7 @@ int main(int, char **) } // Reset pipeline FBOs/Textures due to size change g_pipeline.ResetResources(); + crop_applied_or_cancelled = true; } else { @@ -2196,7 +2803,9 @@ int main(int, char **) { printf("Crop cancelled.\n"); g_cropActive = false; - g_cropRectNorm = ImVec4(0.0f, 0.0f, 1.0f, 1.0f); // Reset to full image + 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; } @@ -2212,10 +2821,16 @@ int main(int, char **) 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; + 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::BeginInspectorPanel( + "Image Inspector", g_loadedImage.m_textureId, + ImVec2(g_loadedImage.m_width, g_loadedImage.m_height), + ImGuiTexInspect::InspectorFlags_NoTooltip); ImGuiTexInspect::EndInspectorPanel(); ImGui::End(); } @@ -2223,13 +2838,16 @@ int main(int, char **) // 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); + 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) + // (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(); @@ -2248,8 +2866,10 @@ int main(int, char **) // 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) + 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 @@ -2259,10 +2879,13 @@ int main(int, char **) 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"); + if (g_histogramResourcesInitialized) + { + if (g_histogramSSBO) + glDeleteBuffers(1, &g_histogramSSBO); + if (g_histogramComputeShader) + glDeleteProgram(g_histogramComputeShader); + printf("Cleaned up histogram resources.\n"); } ImGuiTexInspect::Shutdown(); diff --git a/shaders/diff.frag b/shaders/diff.frag new file mode 100644 index 0000000..d6e315d --- /dev/null +++ b/shaders/diff.frag @@ -0,0 +1,46 @@ +#version 330 core +// Or appropriate GLSL version + +out vec4 FragColor; + +in vec2 TexCoords; // Comes from passthrough.vert + +uniform sampler2D texBefore; +uniform sampler2D texAfter; + +uniform float diffBoost = 5.0; // Uniform to control contrast boost + +// Basic Luma calculation (Rec.709) +float Luma(vec3 color) { + return dot(color, vec3(0.2126, 0.7152, 0.0722)); +} + +void main() { + vec3 colorBefore = texture(texBefore, TexCoords).rgb; + vec3 colorAfter = texture(texAfter, TexCoords).rgb; + + // Calculate absolute difference per channel + vec3 diff = abs(colorAfter - colorBefore); + + // Calculate average difference or luma difference (luma might be better) + // float avgDiff = (diff.r + diff.g + diff.b) / 3.0; + float lumaDiff = abs(Luma(colorAfter) - Luma(colorBefore)); + + // Boost the difference for visibility and clamp + float boostedDiff = clamp(lumaDiff * diffBoost, 0.0, 1.0); + + // Output as grayscale + FragColor = vec4(vec3(boostedDiff), 1.0); + + // --- Alternative Visualizations --- + // // Simple Red highlight for changes: + // if (boostedDiff > 0.05) { // Threshold + // FragColor = vec4(1.0, 0.0, 0.0, 1.0); + // } else { + // FragColor = vec4(colorBefore, 1.0); // Show original where no change + // } + + // // False color based on difference magnitude (example): + // vec3 diffColor = vec3(boostedDiff * 2.0, (1.0 - boostedDiff) * 2.0, 0.0); // Example: Red=High diff, Green=Low diff + // FragColor = vec4(clamp(diffColor, 0.0, 1.0), 1.0); +} \ No newline at end of file