brewery
notashelf /
edae535674ff44a39642d96e343ed421c314fbb1

chroma

public

Lightweight wallpaper daemon for Wayland

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

Commit edae535674ff

tarball

NotAShelf <raf@notashelf.dev> · 2026-05-01 10:10 UTC

unverified4 files changed+108-40
image: implement ref-counted image release

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idb30c621744eb9aa151fcaca012d93cc6a6a6964
diff --git a/include/chroma.h b/include/chroma.hindex 2402d45..f8e2623 100644--- a/include/chroma.h+++ b/include/chroma.h@@ -4,7 +4,7 @@ #include "wlr-layer-shell-unstable-v1.h" #include "xdg-shell.h" #include <EGL/egl.h>-#include <GL/gl.h>+#include <GLES2/gl2.h> #include <signal.h> #include <stdbool.h> #include <stdint.h>@@ -16,7 +16,7 @@  #define MAX_OUTPUTS 16 #define MAX_PATH_LEN 4096-#define CONFIG_FILE_NAME "chroma.conf"+#define CONFIG_FILE_NAME "chroma.toml"  // Log levels #define CHROMA_LOG_ERROR 0@@ -73,6 +73,7 @@ typedef struct {   int channels;   char path[MAX_PATH_LEN];   bool loaded;+  int ref_count; // Number of outputs using this image } chroma_image_t;  // Wayland output information@@ -116,6 +117,7 @@ typedef struct {   bool gl_resources_initialized;   bool texture_uploaded;   bool vbo_dirty; // track VBO needs update+  bool configured; // track if initial configure received } chroma_output_t;  // Config mapping structure@@ -162,6 +164,9 @@ typedef struct chroma_state {   EGLDisplay egl_display;   EGLContext egl_context;   EGLConfig egl_config;+  +  // Shared OpenGL resources+  GLuint shader_program;    // Outputs   chroma_output_t outputs[MAX_OUTPUTS];@@ -235,12 +240,15 @@ void chroma_layer_surface_closed(void *data, // Image loading void chroma_image_init_stb(void); int chroma_image_load(chroma_image_t *image, const char *path,-                      const chroma_config_t *config);+                      const chroma_config_t *config, int output_width,+                      int output_height); void chroma_image_free(chroma_image_t *image);+void chroma_image_release(chroma_image_t *image); chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,                                           const char *path); chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,-                                         const char *path);+                                         const char *path, int output_width,+                                         int output_height); int chroma_image_validate(const char *path); int chroma_image_get_info(const char *path, int *width, int *height,                           int *channels);@@ -248,6 +256,7 @@ void chroma_images_cleanup(chroma_state_t *state);  // Configuration int chroma_config_load(chroma_config_t *config, const char *config_file);+int chroma_config_load_toml(chroma_config_t *config, const char *config_file); void chroma_config_free(chroma_config_t *config); const char *chroma_config_get_image_for_output(chroma_config_t *config,                                                const char *output_name,diff --git a/src/core.c b/src/core.cindex 6751853..9d499ba 100644--- a/src/core.c+++ b/src/core.c@@ -85,8 +85,19 @@ static int assign_wallpaper_to_output(chroma_state_t *state,     return CHROMA_ERROR_CONFIG;   } -  // Load or get cached image-  chroma_image_t *image = chroma_image_get_or_load(state, image_path);+  // Check if image path is empty (no default configured)+  if (strlen(image_path) == 0) {+    chroma_log("WARN",+               "No wallpaper image configured for output %u (%s). "+               "Set default_image in config or provide -c config.toml",+               output->id, output->name ? output->name : "unknown");+    return CHROMA_ERROR_CONFIG;+  }++  // Load or get cached image with output dimensions for intelligent+  // downsampling+  chroma_image_t *image = chroma_image_get_or_load(+      state, image_path, output->width, output->height);   if (!image) {     chroma_log("ERROR", "Failed to load image for output %u: %s", output->id,                image_path);@@ -96,6 +107,7 @@ static int assign_wallpaper_to_output(chroma_state_t *state,   // Check if image changed and invalidate texture cache if neceessary   bool image_changed = (output->image != image);   if (image_changed && output->image) {+    chroma_image_release(output->image);     chroma_output_invalidate_texture(output);     output->vbo_dirty = true; // VBO needs update for new image     chroma_log("DEBUG", "Image changed for output %u, invalidated texture",@@ -161,7 +173,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state,   // Render wallpaper   int ret = chroma_render_wallpaper(state, output);   if (ret != CHROMA_OK) {-    chroma_log("ERROR", "Failed to render wallpaper for output %u", output->id);+    chroma_log("ERROR", "Failed to render wallpaper for output %u: %s", output->id,+               chroma_error_string(ret));     return ret;   } diff --git a/src/image.c b/src/image.cindex 96482c1..ef63c4c 100644--- a/src/image.c+++ b/src/image.c@@ -58,8 +58,9 @@ static void calculate_optimal_size(int original_width, int original_height, // being*. Must be revisited in the future to see how it stands as the program // evolves. static int downsample_image(unsigned char *src_data, int src_width,-                            int src_height, unsigned char *dst_data,-                            int dst_width, int dst_height) {+                            int src_height, int src_channels,+                            unsigned char *dst_data, int dst_width,+                            int dst_height, int dst_channels) {   if (!src_data || !dst_data) {     return -1;   }@@ -77,14 +78,18 @@ static int downsample_image(unsigned char *src_data, int src_width,       src_x = (src_x >= src_width) ? src_width - 1 : src_x;       src_y = (src_y >= src_height) ? src_height - 1 : src_y; -      // Copy pixel (RGBA)-      int src_idx = (src_y * src_width + src_x) * 4;-      int dst_idx = (y * dst_width + x) * 4;+      // Copy pixel data+      int src_idx = (src_y * src_width + src_x) * src_channels;+      int dst_idx = (y * dst_width + x) * dst_channels;        dst_data[dst_idx + 0] = src_data[src_idx + 0]; // R       dst_data[dst_idx + 1] = src_data[src_idx + 1]; // G       dst_data[dst_idx + 2] = src_data[src_idx + 2]; // B-      dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A+      if (dst_channels == 4 && src_channels == 4) {+        dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A+      } else if (dst_channels == 4) {+        dst_data[dst_idx + 3] = 255; // Full alpha for RGB source+      }     }   } @@ -92,8 +97,11 @@ static int downsample_image(unsigned char *src_data, int src_width, }  // Load image from file with configurable downsampling+// output_width/output_height: actual output dimensions for intelligent+// downsampling int chroma_image_load(chroma_image_t *image, const char *path,-                      const chroma_config_t *config) {+                      const chroma_config_t *config, int output_width,+                      int output_height) {   if (!image || !path) {     chroma_log("ERROR", "Invalid parameters for image loading");     return CHROMA_ERROR_INIT;@@ -117,12 +125,24 @@ int chroma_image_load(chroma_image_t *image, const char *path,                (double)file_size / (1024.0 * 1024.0));   } -  // Load image data using stb_image, force RGBA format to avoid conversion+  // Load image data using stb_image+  // First, check actual channels to decide if we need alpha   stbi_set_flip_vertically_on_load(0); // keep images right-side up -  image->data =-      stbi_load(path, &image->width, &image->height, &image->channels, 4);-  image->channels = 4; // always RGBA after forced conversion+  int actual_channels = 0;+  if (!stbi_info(path, &image->width, &image->height, &actual_channels)) {+    chroma_log("ERROR", "Failed to get image info for %s: %s", path,+               stbi_failure_reason());+    return CHROMA_ERROR_IMAGE;+  }++  // Load with actual channels or force RGBA if image has alpha+  // For wallpapers, we typically don't need alpha unless the image has it+  int desired_channels = (actual_channels == 4 || actual_channels == 2) ? 4 : 3;++  image->data = stbi_load(path, &image->width, &image->height, &image->channels,+                          desired_channels);+  image->channels = desired_channels;   if (!image->data) {     chroma_log("ERROR", "Failed to load image %s: %s", path,                stbi_failure_reason());@@ -137,13 +157,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,     return CHROMA_ERROR_IMAGE;   } -  // 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;-  }+  chroma_log("DEBUG", "Loaded image %s with %d channels (original had %d)",+             path, image->channels, actual_channels);    // Store original dimensions before potential downsampling   int original_width = image->width;@@ -155,9 +170,14 @@ int chroma_image_load(chroma_image_t *image, const char *path,   int optimal_height = original_height;    if (config && config->enable_downsampling) {-    calculate_optimal_size(original_width, original_height,-                           config->max_output_width, config->max_output_height,-                           &optimal_width, &optimal_height);+    // Use output dimensions if provided, otherwise fall back to config defaults+    int max_width =+        (output_width > 0) ? output_width : config->max_output_width;+    int max_height =+        (output_height > 0) ? output_height : config->max_output_height;++    calculate_optimal_size(original_width, original_height, max_width,+                           max_height, &optimal_width, &optimal_height);      // Apply minimum scale factor constraint     float scale_x = (float)optimal_width / (float)original_width;@@ -178,7 +198,7 @@ int chroma_image_load(chroma_image_t *image, const char *path,         (optimal_width < original_width || optimal_height < original_height);   } -  // Downsamp if needed and enabled+  // Downsample if needed and enabled   if (should_downsample) {     double reduction_ratio = (double)(optimal_width * optimal_height) /                              (double)(original_width * original_height) * 100.0;@@ -187,7 +207,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,                original_width, original_height, optimal_width, optimal_height,                reduction_ratio); -    size_t optimal_size = (size_t)optimal_width * (size_t)optimal_height * 4;+    size_t optimal_size =+        (size_t)optimal_width * (size_t)optimal_height * image->channels;     unsigned char *downsampled_data = malloc(optimal_size);     if (!downsampled_data) {       chroma_log("ERROR", "Failed to allocate memory for downsampled image");@@ -196,8 +217,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,     }      if (downsample_image(image->data, original_width, original_height,-                         downsampled_data, optimal_width,-                         optimal_height) != 0) {+                         image->channels, downsampled_data, optimal_width,+                         optimal_height, image->channels) != 0) {       chroma_log("ERROR", "Failed to downsample image");       free(downsampled_data);       chroma_image_free(image);@@ -218,6 +239,7 @@ int chroma_image_load(chroma_image_t *image, const char *path,   }    image->loaded = true;+  image->ref_count = 1; // Initial reference from the first output    // Calculate and log memory allocation   size_t image_size =@@ -258,6 +280,7 @@ void chroma_image_free(chroma_image_t *image) {   image->height = 0;   image->channels = 0;   image->loaded = false;+  image->ref_count = 0;    if (strlen(image->path) > 0) {     chroma_log("DEBUG", "Freed image: %s", image->path);@@ -266,6 +289,18 @@ void chroma_image_free(chroma_image_t *image) {   memset(image->path, 0, sizeof(image->path)); } +// Release a reference to an image; free when ref_count reaches zero+void chroma_image_release(chroma_image_t *image) {+  if (!image) {+    return;+  }++  image->ref_count--;+  if (image->ref_count <= 0) {+    chroma_image_free(image);+  }+}+ // Find image by path in state chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,                                           const char *path) {@@ -283,8 +318,11 @@ chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, }  // Load image if not already loaded+// output_width/output_height: actual output dimensions for intelligent+// downsampling chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,-                                         const char *path) {+                                         const char *path, int output_width,+                                         int output_height) {   if (!state || !path) {     return NULL;   }@@ -292,7 +330,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,   // Check if already loaded   chroma_image_t *existing = chroma_image_find_by_path(state, path);   if (existing && existing->loaded) {-    chroma_log("DEBUG", "Using cached image: %s", path);+    chroma_log("DEBUG", "Using cached image: %s (ref_count: %d)", path,+               existing->ref_count);+    existing->ref_count++;     return existing;   } @@ -307,8 +347,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,     state->image_count++;   } -  // Load the image with configuration-  if (chroma_image_load(image, path, &state->config) != CHROMA_OK) {+  // Load the image with configuration and output dimensions+  if (chroma_image_load(image, path, &state->config, output_width,+                        output_height) != CHROMA_OK) {     // If this was a new slot, decrement count     if (!existing) {       state->image_count--;@@ -376,7 +417,7 @@ void chroma_images_cleanup(chroma_state_t *state) {   chroma_log("DEBUG", "Cleaning up %d images", state->image_count);    for (int i = 0; i < state->image_count; i++) {-    chroma_image_free(&state->images[i]);+    chroma_image_release(&state->images[i]);   }    state->image_count = 0;diff --git a/src/wayland.c b/src/wayland.cindex e931511..c5fa252 100644--- a/src/wayland.c+++ b/src/wayland.c@@ -91,9 +91,8 @@ static void layer_surface_configure(void *data,   chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u",              output->id, serial); -  // Commit the surface to apply the acknowledgment-  wl_surface_commit(output->surface);-  chroma_log("TRACE", "Surface committed for output %u", output->id);+  // Mark as configured - actual commit happens in render via eglSwapBuffers+  output->configured = true;    chroma_log("DEBUG", "Acknowledged layer surface configure for output %u",              output->id);@@ -382,6 +381,12 @@ void chroma_output_remove(chroma_state_t *state, uint32_t id) {   chroma_log("INFO", "Removing output %u (%s)", id,              output->name ? output->name : "unknown"); +  // Release image reference if the output holds one+  if (output->image) {+    chroma_image_release(output->image);+    output->image = NULL;+  }+   // Clean up surface if it exists   if (output->surface) {     chroma_surface_destroy(output);