diff --git a/include/chroma.h b/include/chroma.h index 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 -#include +#include #include #include #include @@ -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.c index 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.c index 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.c index 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);