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 "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,

View file

@ -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;
}

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
// 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;

View file

@ -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);