brewery
notashelf /
d1116e772101b82b426c7ea946b726022a9271e5

chroma

public

Lightweight wallpaper daemon for Wayland

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/chroma.git

Commit d1116e772101

tarball

NotAShelf <raf@notashelf.dev> · 2025-09-30 17:11 UTC

unverified4 files changed+221-114
render: add OpenGL resource caching; optimize texture handling

Mildly improves rendering performance by caching OpenGL resources.
Namely:

- Cache shader program, VBO/EBO, and textures per output
- Automatically free image data after GPU upload
- Force RGBA format for consistent texture handling
- Track texture state across output changes
- Add texture invalidation on image changes

This reduces the memory usage by a solid 30MB, but it's still not quite
enough. I expect (or rather, hope) that we can cut it by half.

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6a6a6964eebc783c5bc07b1fef7548a8d49f529c
diff --git a/include/chroma.h b/include/chroma.hindex 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_Hdiff --git a/src/core.c b/src/core.cindex 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.cindex 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.cindex 1537fe5..dda4030 100644--- a/src/render.c+++ b/src/render.c@@ -1,4 +1,3 @@-#include <math.h> #include <stdio.h> #include <stdlib.h> #include <string.h>@@ -7,6 +6,7 @@ #include <GLES2/gl2.h>  #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);--  // Create vertex buffer objects (should be cached in real implementation)-  GLuint vbo, ebo;-  glGenBuffers(1, &vbo);-  glGenBuffers(1, &ebo);+  glBindTexture(GL_TEXTURE_2D, output->texture_id);+  glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0); -  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);+  }+}