diff --git a/include/chroma.h b/include/chroma.h index e15946a..ba36ce5 100644 --- a/include/chroma.h +++ b/include/chroma.h @@ -62,6 +62,14 @@ typedef struct { // Associated wallpaper chroma_image_t *image; + + // OpenGL resource cache + GLuint texture_id; + GLuint shader_program; + GLuint vbo; + GLuint ebo; + bool gl_resources_initialized; + bool texture_uploaded; } chroma_output_t; // Config mapping structure @@ -150,6 +158,7 @@ void chroma_egl_cleanup(chroma_state_t *state); int chroma_surface_create(chroma_state_t *state, chroma_output_t *output); void chroma_surface_destroy(chroma_output_t *output); int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output); +void chroma_output_invalidate_texture(chroma_output_t *output); // Layer shell functions void chroma_layer_surface_configure(void *data, @@ -219,4 +228,4 @@ extern const struct zwlr_layer_surface_v1_listener // Global state for signal handling extern volatile sig_atomic_t chroma_should_quit; -#endif // CHROMA_H \ No newline at end of file +#endif // CHROMA_H diff --git a/src/core.c b/src/core.c index b97537f..f07b8ee 100644 --- a/src/core.c +++ b/src/core.c @@ -87,6 +87,14 @@ static int assign_wallpaper_to_output(chroma_state_t *state, return CHROMA_ERROR_IMAGE; } + // Check if image changed and invalidate texture cache if neceessary + bool image_changed = (output->image != image); + if (image_changed && output->image) { + chroma_output_invalidate_texture(output); + chroma_log("DEBUG", "Image changed for output %u, invalidated texture", + output->id); + } + // Assign image to output output->image = image; diff --git a/src/image.c b/src/image.c index c8c9770..e267728 100644 --- a/src/image.c +++ b/src/image.c @@ -49,11 +49,12 @@ int chroma_image_load(chroma_image_t *image, const char *path) { (double)file_size / (1024.0 * 1024.0)); } - // Load image data using stb_image - stbi_set_flip_vertically_on_load(0); // Keep images right-side up + // Load image data using stb_image, force RGBA format to avoid conversion + stbi_set_flip_vertically_on_load(0); // keep images right-side up image->data = - stbi_load(path, &image->width, &image->height, &image->channels, 0); + stbi_load(path, &image->width, &image->height, &image->channels, 4); + image->channels = 4; // always RGBA after forced conversion if (!image->data) { chroma_log("ERROR", "Failed to load image %s: %s", path, stbi_failure_reason()); @@ -68,39 +69,14 @@ int chroma_image_load(chroma_image_t *image, const char *path) { return CHROMA_ERROR_IMAGE; } - // Check supported formats - if (image->channels < 3 || image->channels > 4) { - chroma_log("ERROR", - "Unsupported image format: %d channels (need RGB or RGBA)", + // Validate we have RGBA data (should always be true with forced conversion) + if (image->channels != 4) { + chroma_log("ERROR", "Failed to load image as RGBA: got %d channels", image->channels); chroma_image_free(image); return CHROMA_ERROR_IMAGE; } - // Convert RGB to RGBA if necessary for consistent handling - if (image->channels == 3) { - int pixel_count = image->width * image->height; - unsigned char *rgba_data = malloc(pixel_count * 4); - if (!rgba_data) { - chroma_log("ERROR", "Failed to allocate memory for RGBA conversion"); - chroma_image_free(image); - return CHROMA_ERROR_MEMORY; - } - - // Convert RGB to RGBA - for (int i = 0; i < pixel_count; i++) { - rgba_data[i * 4 + 0] = image->data[i * 3 + 0]; // R - rgba_data[i * 4 + 1] = image->data[i * 3 + 1]; // G - rgba_data[i * 4 + 2] = image->data[i * 3 + 2]; // B - rgba_data[i * 4 + 3] = 255; // A - } - - // Replace original data - stbi_image_free(image->data); - image->data = rgba_data; - image->channels = 4; - } - image->loaded = true; chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)", path, @@ -118,13 +94,8 @@ void chroma_image_free(chroma_image_t *image) { } if (image->data) { - if (image->channels == 4 && strlen(image->path) > 0) { - // If we converted from RGB to RGBA, use regular free() - free(image->data); - } else { - // If loaded directly by stb_image, use stbi_image_free() - stbi_image_free(image->data); - } + // Always use stbi_image_free since we load directly with stbi_load + stbi_image_free(image->data); image->data = NULL; } @@ -265,4 +236,4 @@ void chroma_image_init_stb(void) { stbi_ldr_to_hdr_scale(1.0f); chroma_log("DEBUG", "Initialized stb_image library"); -} \ No newline at end of file +} diff --git a/src/render.c b/src/render.c index 1537fe5..dda4030 100644 --- a/src/render.c +++ b/src/render.c @@ -1,4 +1,3 @@ -#include #include #include #include @@ -7,6 +6,7 @@ #include #include "../include/chroma.h" +#include "../include/stb_image.h" // Vertex shader for simple texture rendering static const char *vertex_shader_source = @@ -31,18 +31,17 @@ static const char *fragment_shader_source = // Vertices for a fullscreen quad static const float vertices[] = { // Position Texcoord - -1.0f, -1.0f, 0.0f, 1.0f, // Bottom left - 1.0f, -1.0f, 1.0f, 1.0f, // Bottom right - 1.0f, 1.0f, 1.0f, 0.0f, // Top right - -1.0f, 1.0f, 0.0f, 0.0f, // Top left + -1.0f, -1.0f, 0.0f, 1.0f, // bottom left + 1.0f, -1.0f, 1.0f, 1.0f, // bottom right + 1.0f, 1.0f, 1.0f, 0.0f, // top right + -1.0f, 1.0f, 0.0f, 0.0f, // top left }; static const unsigned int indices[] = { - 0, 1, 2, // First triangle - 2, 3, 0 // Second triangle + 0, 1, 2, // first triangle + 2, 3, 0 // second triangle }; -// Shader compilation helper static GLuint compile_shader(GLenum type, const char *source) { GLuint shader = glCreateShader(type); if (!shader) { @@ -66,7 +65,6 @@ static GLuint compile_shader(GLenum type, const char *source) { return shader; } -// Shader program creation helper static GLuint create_shader_program(void) { GLuint vertex_shader = compile_shader(GL_VERTEX_SHADER, vertex_shader_source); if (!vertex_shader) { @@ -108,6 +106,145 @@ static GLuint create_shader_program(void) { return program; } +// Initialize OpenGL resources for output +static int init_gl_resources(chroma_output_t *output) { + if (!output || output->gl_resources_initialized) { + return CHROMA_OK; + } + + // Create shader prog + output->shader_program = create_shader_program(); + if (!output->shader_program) { + chroma_log("ERROR", "Failed to create shader program for output %u", + output->id); + return CHROMA_ERROR_EGL; + } + + // Create and setup VBO/EBO + glGenBuffers(1, &output->vbo); + glGenBuffers(1, &output->ebo); + + glBindBuffer(GL_ARRAY_BUFFER, output->vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, + GL_STATIC_DRAW); + + output->texture_id = 0; // will be created when image is assigned + output->gl_resources_initialized = true; + + chroma_log("DEBUG", "Initialized GL resources for output %u", output->id); + return CHROMA_OK; +} + +// Create or update texture from image data +static int update_texture_from_image(chroma_output_t *output, + chroma_image_t *image) { + if (!output || !image || !image->loaded) { + return CHROMA_ERROR_INIT; + } + + // If image data was already freed after previous GPU upload, we can't upload + // again + if (!image->data) { + chroma_log("ERROR", + "Cannot create texture: image data already freed for %s", + image->path); + return CHROMA_ERROR_IMAGE; + } + + // Delete existing texture if it exists + if (output->texture_id != 0) { + glDeleteTextures(1, &output->texture_id); + output->texture_id = 0; + } + + // Create new texture + glGenTextures(1, &output->texture_id); + glBindTexture(GL_TEXTURE_2D, output->texture_id); + + // Set texture parameters + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + + // Upload texture data (always RGBA now) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0, + GL_RGBA, GL_UNSIGNED_BYTE, image->data); + + glBindTexture(GL_TEXTURE_2D, 0); + + // Mark this output as having uploaded its texture + output->texture_uploaded = true; + + // Free system RAM copy only when ALL outputs using this image have uploaded + // to GPU + if (image->data) { + // Count total outputs using this image and how many have uploaded + int total_using = 0; + int uploaded_count = 0; + + chroma_state_t *state = output->state; + for (int i = 0; i < state->output_count; i++) { + if (state->outputs[i].active && state->outputs[i].image == image) { + total_using++; + if (state->outputs[i].texture_uploaded) { + uploaded_count++; + } + } + } + + // Only free image data when ALL outputs using it have uploaded + if (total_using > 0 && uploaded_count >= total_using) { + size_t freed_bytes = + (size_t)image->width * image->height * image->channels; + stbi_image_free(image->data); + image->data = NULL; + chroma_log("INFO", + "Freed %.2f MB of image data after all %d outputs uploaded to " + "GPU: %s", + (double)freed_bytes / (1024.0 * 1024.0), total_using, + image->path); + } + } + + chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id, + image->width, image->height); + return CHROMA_OK; +} + +// Cleanup OpenGL resources for output +static void cleanup_gl_resources(chroma_output_t *output) { + if (!output || !output->gl_resources_initialized) { + return; + } + + if (output->texture_id != 0) { + glDeleteTextures(1, &output->texture_id); + output->texture_id = 0; + } + + if (output->shader_program != 0) { + glDeleteProgram(output->shader_program); + output->shader_program = 0; + } + + if (output->vbo != 0) { + glDeleteBuffers(1, &output->vbo); + output->vbo = 0; + } + + if (output->ebo != 0) { + glDeleteBuffers(1, &output->ebo); + output->ebo = 0; + } + + output->gl_resources_initialized = false; + chroma_log("DEBUG", "Cleaned up GL resources for output %u", output->id); +} + // EGL configuration selection static int choose_egl_config(EGLDisplay display, EGLConfig *config) { EGLint attributes[] = {EGL_SURFACE_TYPE, @@ -292,6 +429,9 @@ void chroma_surface_destroy(chroma_output_t *output) { return; } + // Clean up OpenGL resources first + cleanup_gl_resources(output); + if (output->egl_surface != EGL_NO_SURFACE) { eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface); output->egl_surface = EGL_NO_SURFACE; @@ -315,33 +455,7 @@ void chroma_surface_destroy(chroma_output_t *output) { chroma_log("DEBUG", "Destroyed surface for output %u", output->id); } -// Create texture from image data -static GLuint create_texture_from_image(chroma_image_t *image) { - if (!image || !image->loaded || !image->data) { - return 0; - } - - GLuint texture; - glGenTextures(1, &texture); - glBindTexture(GL_TEXTURE_2D, texture); - - // Set texture parameters - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - - // Upload texture data - GLenum format = (image->channels == 4) ? GL_RGBA : GL_RGB; - glTexImage2D(GL_TEXTURE_2D, 0, format, image->width, image->height, 0, format, - GL_UNSIGNED_BYTE, image->data); - - glBindTexture(GL_TEXTURE_2D, 0); - - return texture; -} - -// Render wallpaper to output +// Render wallpaper to output using cached resources int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { if (!state || !output || !output->image || !output->image->loaded) { return CHROMA_ERROR_INIT; @@ -355,6 +469,17 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { return CHROMA_ERROR_EGL; } + if (init_gl_resources(output) != CHROMA_OK) { + return CHROMA_ERROR_EGL; + } + + if (output->texture_id == 0) { + if (update_texture_from_image(output, output->image) != CHROMA_OK) { + chroma_log("ERROR", "Failed to update texture for output %u", output->id); + return CHROMA_ERROR_EGL; + } + } + // Set viewport glViewport(0, 0, output->width, output->height); @@ -362,42 +487,21 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); - // Create shader program (should be cached in real implementation) - GLuint program = create_shader_program(); - if (!program) { - return CHROMA_ERROR_EGL; - } - - // Use shader program - glUseProgram(program); - - // Create and bind texture - GLuint texture = create_texture_from_image(output->image); - if (!texture) { - chroma_log("ERROR", "Failed to create texture for output %u", output->id); - glDeleteProgram(program); - return CHROMA_ERROR_EGL; - } + // Use cached shader program + glUseProgram(output->shader_program); + // Bind cached texture glActiveTexture(GL_TEXTURE0); - glBindTexture(GL_TEXTURE_2D, texture); - glUniform1i(glGetUniformLocation(program, "texture"), 0); + glBindTexture(GL_TEXTURE_2D, output->texture_id); + glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0); - // Create vertex buffer objects (should be cached in real implementation) - GLuint vbo, ebo; - glGenBuffers(1, &vbo); - glGenBuffers(1, &ebo); - - glBindBuffer(GL_ARRAY_BUFFER, vbo); - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); - - glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); - glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, - GL_STATIC_DRAW); + // Use cached VBO/EBO + glBindBuffer(GL_ARRAY_BUFFER, output->vbo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo); // Set vertex attributes - GLint position_attr = glGetAttribLocation(program, "position"); - GLint texcoord_attr = glGetAttribLocation(program, "texcoord"); + GLint position_attr = glGetAttribLocation(output->shader_program, "position"); + GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord"); glEnableVertexAttribArray(position_attr); glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), @@ -410,11 +514,11 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { // Draw glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); - // Cleanup - glDeleteBuffers(1, &vbo); - glDeleteBuffers(1, &ebo); - glDeleteTextures(1, &texture); - glDeleteProgram(program); + // Unbind resources + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); + glBindTexture(GL_TEXTURE_2D, 0); + glUseProgram(0); // Swap buffers if (!eglSwapBuffers(state->egl_display, output->egl_surface)) { @@ -426,6 +530,21 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) { // Commit surface wl_surface_commit(output->surface); - chroma_log("DEBUG", "Rendered wallpaper to output %u", output->id); + chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)", + output->id); return CHROMA_OK; -} \ No newline at end of file +} + +// Invalidate texture cache for output +void chroma_output_invalidate_texture(chroma_output_t *output) { + if (!output || !output->gl_resources_initialized) { + return; + } + + if (output->texture_id != 0) { + glDeleteTextures(1, &output->texture_id); + output->texture_id = 0; + output->texture_uploaded = false; // reset upload flag + chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id); + } +}