image: implement ref-counted image release

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idb30c621744eb9aa151fcaca012d93cc6a6a6964
This commit is contained in:
raf 2026-04-27 22:03:12 +03:00
commit edae535674
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
4 changed files with 108 additions and 40 deletions

17
include/chroma.h vendored
View file

@ -4,7 +4,7 @@
#include "wlr-layer-shell-unstable-v1.h" #include "wlr-layer-shell-unstable-v1.h"
#include "xdg-shell.h" #include "xdg-shell.h"
#include <EGL/egl.h> #include <EGL/egl.h>
#include <GL/gl.h> #include <GLES2/gl2.h>
#include <signal.h> #include <signal.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@ -16,7 +16,7 @@
#define MAX_OUTPUTS 16 #define MAX_OUTPUTS 16
#define MAX_PATH_LEN 4096 #define MAX_PATH_LEN 4096
#define CONFIG_FILE_NAME "chroma.conf" #define CONFIG_FILE_NAME "chroma.toml"
// Log levels // Log levels
#define CHROMA_LOG_ERROR 0 #define CHROMA_LOG_ERROR 0
@ -73,6 +73,7 @@ typedef struct {
int channels; int channels;
char path[MAX_PATH_LEN]; char path[MAX_PATH_LEN];
bool loaded; bool loaded;
int ref_count; // Number of outputs using this image
} chroma_image_t; } chroma_image_t;
// Wayland output information // Wayland output information
@ -116,6 +117,7 @@ typedef struct {
bool gl_resources_initialized; bool gl_resources_initialized;
bool texture_uploaded; bool texture_uploaded;
bool vbo_dirty; // track VBO needs update bool vbo_dirty; // track VBO needs update
bool configured; // track if initial configure received
} chroma_output_t; } chroma_output_t;
// Config mapping structure // Config mapping structure
@ -162,6 +164,9 @@ typedef struct chroma_state {
EGLDisplay egl_display; EGLDisplay egl_display;
EGLContext egl_context; EGLContext egl_context;
EGLConfig egl_config; EGLConfig egl_config;
// Shared OpenGL resources
GLuint shader_program;
// Outputs // Outputs
chroma_output_t outputs[MAX_OUTPUTS]; chroma_output_t outputs[MAX_OUTPUTS];
@ -235,12 +240,15 @@ void chroma_layer_surface_closed(void *data,
// Image loading // Image loading
void chroma_image_init_stb(void); void chroma_image_init_stb(void);
int chroma_image_load(chroma_image_t *image, const char *path, 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_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, chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
const char *path); const char *path);
chroma_image_t *chroma_image_get_or_load(chroma_state_t *state, 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_validate(const char *path);
int chroma_image_get_info(const char *path, int *width, int *height, int chroma_image_get_info(const char *path, int *width, int *height,
int *channels); int *channels);
@ -248,6 +256,7 @@ void chroma_images_cleanup(chroma_state_t *state);
// Configuration // Configuration
int chroma_config_load(chroma_config_t *config, const char *config_file); 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); void chroma_config_free(chroma_config_t *config);
const char *chroma_config_get_image_for_output(chroma_config_t *config, const char *chroma_config_get_image_for_output(chroma_config_t *config,
const char *output_name, const char *output_name,

View file

@ -85,8 +85,19 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
return CHROMA_ERROR_CONFIG; return CHROMA_ERROR_CONFIG;
} }
// Load or get cached image // Check if image path is empty (no default configured)
chroma_image_t *image = chroma_image_get_or_load(state, image_path); 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) { if (!image) {
chroma_log("ERROR", "Failed to load image for output %u: %s", output->id, chroma_log("ERROR", "Failed to load image for output %u: %s", output->id,
image_path); 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 // Check if image changed and invalidate texture cache if neceessary
bool image_changed = (output->image != image); bool image_changed = (output->image != image);
if (image_changed && output->image) { if (image_changed && output->image) {
chroma_image_release(output->image);
chroma_output_invalidate_texture(output); chroma_output_invalidate_texture(output);
output->vbo_dirty = true; // VBO needs update for new image output->vbo_dirty = true; // VBO needs update for new image
chroma_log("DEBUG", "Image changed for output %u, invalidated texture", 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 // Render wallpaper
int ret = chroma_render_wallpaper(state, output); int ret = chroma_render_wallpaper(state, output);
if (ret != CHROMA_OK) { 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; return ret;
} }

View file

@ -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 // being*. Must be revisited in the future to see how it stands as the program
// evolves. // evolves.
static int downsample_image(unsigned char *src_data, int src_width, static int downsample_image(unsigned char *src_data, int src_width,
int src_height, unsigned char *dst_data, int src_height, int src_channels,
int dst_width, int dst_height) { unsigned char *dst_data, int dst_width,
int dst_height, int dst_channels) {
if (!src_data || !dst_data) { if (!src_data || !dst_data) {
return -1; 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_x = (src_x >= src_width) ? src_width - 1 : src_x;
src_y = (src_y >= src_height) ? src_height - 1 : src_y; src_y = (src_y >= src_height) ? src_height - 1 : src_y;
// Copy pixel (RGBA) // Copy pixel data
int src_idx = (src_y * src_width + src_x) * 4; int src_idx = (src_y * src_width + src_x) * src_channels;
int dst_idx = (y * dst_width + x) * 4; int dst_idx = (y * dst_width + x) * dst_channels;
dst_data[dst_idx + 0] = src_data[src_idx + 0]; // R 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 + 1] = src_data[src_idx + 1]; // G
dst_data[dst_idx + 2] = src_data[src_idx + 2]; // B 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 // 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, 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) { if (!image || !path) {
chroma_log("ERROR", "Invalid parameters for image loading"); chroma_log("ERROR", "Invalid parameters for image loading");
return CHROMA_ERROR_INIT; 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)); (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 stbi_set_flip_vertically_on_load(0); // keep images right-side up
image->data = int actual_channels = 0;
stbi_load(path, &image->width, &image->height, &image->channels, 4); if (!stbi_info(path, &image->width, &image->height, &actual_channels)) {
image->channels = 4; // always RGBA after forced conversion 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) { if (!image->data) {
chroma_log("ERROR", "Failed to load image %s: %s", path, chroma_log("ERROR", "Failed to load image %s: %s", path,
stbi_failure_reason()); stbi_failure_reason());
@ -137,13 +157,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,
return CHROMA_ERROR_IMAGE; return CHROMA_ERROR_IMAGE;
} }
// Validate we have RGBA data (should always be true with forced conversion) chroma_log("DEBUG", "Loaded image %s with %d channels (original had %d)",
if (image->channels != 4) { path, image->channels, actual_channels);
chroma_log("ERROR", "Failed to load image as RGBA: got %d channels",
image->channels);
chroma_image_free(image);
return CHROMA_ERROR_IMAGE;
}
// Store original dimensions before potential downsampling // Store original dimensions before potential downsampling
int original_width = image->width; 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; int optimal_height = original_height;
if (config && config->enable_downsampling) { if (config && config->enable_downsampling) {
calculate_optimal_size(original_width, original_height, // Use output dimensions if provided, otherwise fall back to config defaults
config->max_output_width, config->max_output_height, int max_width =
&optimal_width, &optimal_height); (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 // Apply minimum scale factor constraint
float scale_x = (float)optimal_width / (float)original_width; 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); (optimal_width < original_width || optimal_height < original_height);
} }
// Downsamp if needed and enabled // Downsample if needed and enabled
if (should_downsample) { if (should_downsample) {
double reduction_ratio = (double)(optimal_width * optimal_height) / double reduction_ratio = (double)(optimal_width * optimal_height) /
(double)(original_width * original_height) * 100.0; (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, original_width, original_height, optimal_width, optimal_height,
reduction_ratio); 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); unsigned char *downsampled_data = malloc(optimal_size);
if (!downsampled_data) { if (!downsampled_data) {
chroma_log("ERROR", "Failed to allocate memory for downsampled image"); 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, if (downsample_image(image->data, original_width, original_height,
downsampled_data, optimal_width, image->channels, downsampled_data, optimal_width,
optimal_height) != 0) { optimal_height, image->channels) != 0) {
chroma_log("ERROR", "Failed to downsample image"); chroma_log("ERROR", "Failed to downsample image");
free(downsampled_data); free(downsampled_data);
chroma_image_free(image); chroma_image_free(image);
@ -218,6 +239,7 @@ int chroma_image_load(chroma_image_t *image, const char *path,
} }
image->loaded = true; image->loaded = true;
image->ref_count = 1; // Initial reference from the first output
// Calculate and log memory allocation // Calculate and log memory allocation
size_t image_size = size_t image_size =
@ -258,6 +280,7 @@ void chroma_image_free(chroma_image_t *image) {
image->height = 0; image->height = 0;
image->channels = 0; image->channels = 0;
image->loaded = false; image->loaded = false;
image->ref_count = 0;
if (strlen(image->path) > 0) { if (strlen(image->path) > 0) {
chroma_log("DEBUG", "Freed image: %s", image->path); 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)); 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 // Find image by path in state
chroma_image_t *chroma_image_find_by_path(chroma_state_t *state, chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
const char *path) { 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 // 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, 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) { if (!state || !path) {
return NULL; return NULL;
} }
@ -292,7 +330,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
// Check if already loaded // Check if already loaded
chroma_image_t *existing = chroma_image_find_by_path(state, path); chroma_image_t *existing = chroma_image_find_by_path(state, path);
if (existing && existing->loaded) { 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; return existing;
} }
@ -307,8 +347,9 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
state->image_count++; state->image_count++;
} }
// Load the image with configuration // Load the image with configuration and output dimensions
if (chroma_image_load(image, path, &state->config) != CHROMA_OK) { if (chroma_image_load(image, path, &state->config, output_width,
output_height) != CHROMA_OK) {
// If this was a new slot, decrement count // If this was a new slot, decrement count
if (!existing) { if (!existing) {
state->image_count--; 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); chroma_log("DEBUG", "Cleaning up %d images", state->image_count);
for (int i = 0; i < state->image_count; i++) { 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; state->image_count = 0;

View file

@ -91,9 +91,8 @@ static void layer_surface_configure(void *data,
chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u", chroma_log("TRACE", "Sent configure acknowledgment for output %u serial %u",
output->id, serial); output->id, serial);
// Commit the surface to apply the acknowledgment // Mark as configured - actual commit happens in render via eglSwapBuffers
wl_surface_commit(output->surface); output->configured = true;
chroma_log("TRACE", "Surface committed for output %u", output->id);
chroma_log("DEBUG", "Acknowledged layer surface configure for output %u", chroma_log("DEBUG", "Acknowledged layer surface configure for output %u",
output->id); 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, chroma_log("INFO", "Removing output %u (%s)", id,
output->name ? output->name : "unknown"); 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 // Clean up surface if it exists
if (output->surface) { if (output->surface) {
chroma_surface_destroy(output); chroma_surface_destroy(output);