Compare commits

..

1 commit

Author SHA1 Message Date
cc6fb94e5a build: tag 1.1.0
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I887f6c57dc16cd697061b995beab4a236a6a6964
2026-04-21 16:51:40 +03:00
17 changed files with 647 additions and 3687 deletions

2
.gitattributes vendored
View file

@ -1,7 +1,7 @@
# Vendored headers are vendored code, to the surprise of absolutely noone. # Vendored headers are vendored code, to the surprise of absolutely noone.
# See: # See:
# <https://github.com/github-linguist/linguist/blob/main/docs/overrides.md#vendored-code> # <https://github.com/github-linguist/linguist/blob/main/docs/overrides.md#vendored-code>
/include/**/* linguist-vendored /include/chroma.h linguist-vendored
# Don't think linguist can detect XML, but let's tell it that the protocols are # Don't think linguist can detect XML, but let's tell it that the protocols are
# vendored *anyway*. # vendored *anyway*.

View file

@ -1,5 +1,5 @@
PROJECT_NAME = chroma PROJECT_NAME = chroma
VERSION = 2.0.0 VERSION = 1.1.0
# Directories # Directories
SRCDIR = src SRCDIR = src
@ -14,9 +14,6 @@ PREFIX ?= /usr/local
BINDIR_INSTALL = $(PREFIX)/bin BINDIR_INSTALL = $(PREFIX)/bin
SYSTEMD_INSTALL = $(HOME)/.config/systemd/user SYSTEMD_INSTALL = $(HOME)/.config/systemd/user
# Config file
CONFIG_FILE_NAME = chroma.toml
# Compiler and flags # Compiler and flags
CC = gcc CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g
@ -49,11 +46,8 @@ LDFLAGS += -lm -ldl
# Source files (excluding generated protocol files) # Source files (excluding generated protocol files)
SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c)) SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))
VENDOR_SOURCES = $(INCDIR)/vendor/tomlc17.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS) OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)
VENDOR_OBJECTS = $(VENDOR_SOURCES:$(INCDIR)/vendor/%.c=$(OBJDIR)/%.o) DEPENDS = $(OBJECTS:.o=.d)
ALL_OBJECTS = $(OBJECTS) $(VENDOR_OBJECTS)
DEPENDS = $(ALL_OBJECTS:.o=.d)
# Override object files for image.c and render.c to suppress third-party warnings # Override object files for image.c and render.c to suppress third-party warnings
OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS)) OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS))
@ -84,9 +78,9 @@ $(INCDIR):
@mkdir -p $(INCDIR) @mkdir -p $(INCDIR)
# Build main executable # Build main executable
$(TARGET): version-header $(PROTOCOL_HEADERS) $(ALL_OBJECTS) | $(BINDIR) $(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
@echo " LINK $@" @echo " LINK $@"
@$(CC) $(ALL_OBJECTS) -o $@ $(LDFLAGS) @$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
# Compile source files # Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR) $(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@ -101,11 +95,6 @@ $(OBJDIR)/render.o: $(SRCDIR)/render.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@echo " CC $<" @echo " CC $<"
@$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@ @$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@
# Compile vendor files
$(OBJDIR)/%.o: $(INCDIR)/vendor/%.c | $(OBJDIR)
@echo " CC $<"
@$(CC) $(CPPFLAGS) $(CFLAGS) -w -MMD -MP -c $< -o $@
# Debug build # Debug build
debug: CFLAGS = $(DEBUG_CFLAGS) debug: CFLAGS = $(DEBUG_CFLAGS)
debug: $(TARGET) debug: $(TARGET)
@ -153,11 +142,6 @@ version-header:
# Create systemd service file # Create systemd service file
systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service systemd-service: $(SYSTEMD_DIR)/$(PROJECT_NAME).service
# Create sample configuration file
sample-config:
@echo "Creating sample configuration..."
@cp chroma.toml.sample $(CONFIG_FILE_NAME)
@echo "Sample configuration created at $(CONFIG_FILE_NAME)"
# Clean build artifacts # Clean build artifacts
clean: clean:
@ -246,7 +230,6 @@ help:
@echo " check-deps - Check if all dependencies are available" @echo " check-deps - Check if all dependencies are available"
@echo " install - Install executable and systemd service" @echo " install - Install executable and systemd service"
@echo " uninstall - Remove installed files" @echo " uninstall - Remove installed files"
@echo " sample-config - Create sample configuration file"
@echo " clean - Remove build artifacts" @echo " clean - Remove build artifacts"
@echo " distclean - Remove all generated files" @echo " distclean - Remove all generated files"
@echo " format - Format source code (requires clang-format)" @echo " format - Format source code (requires clang-format)"
@ -271,7 +254,7 @@ help:
-include $(DEPENDS) -include $(DEPENDS)
# Phony targets # Phony targets
.PHONY: all debug static check-deps install uninstall systemd-service sample-config version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version .PHONY: all debug static check-deps install uninstall systemd-service version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version
# Print variables # Print variables
print-%: print-%:

View file

@ -28,7 +28,7 @@ comprehensive monitor management. Here's what makes Chroma stand out:
- **Multiple image formats**: Supports JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, - **Multiple image formats**: Supports JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC,
PPM, PGM PPM, PGM
- **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering - **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering
- **Simple Configuration file**: Easy setup with TOML config files - **Configuration file support**: Easy setup with INI-style config files
- **Signal handling**: Graceful shutdown and configuration reload (SIGHUP) - **Signal handling**: Graceful shutdown and configuration reload (SIGHUP)
- **Intelligent downsampling**: Automatically reduces large image resolution to - **Intelligent downsampling**: Automatically reduces large image resolution to
save memory (up to 94% memory savings for 8K images) save memory (up to 94% memory savings for 8K images)
@ -96,40 +96,21 @@ make sample-config
Chroma looks for configuration files in this order: Chroma looks for configuration files in this order:
1. `$XDG_CONFIG_HOME/chroma/chroma.toml` 1. `~/.config/chroma/chroma.conf`
2. `~/.config/chroma/chroma.toml` 2. `$XDG_CONFIG_HOME/chroma/chroma.conf`
3. `./chroma.toml` (current directory) 3. `./chroma.conf` (current directory)
#### Sample Configuration #### Sample Configuration
```toml ```ini
# Default wallpaper for outputs without specific mapping # Default wallpaper for outputs without specific mapping
default_image = "~/.config/chroma/default.jpg" default_image = ~/.config/chroma/default.jpg
# Output-specific wallpapers # Output-specific wallpapers
# Format: [[output]] with name and image keys # Format: output.OUTPUT_NAME = /path/to/image.jpg
[[output]] output.DP-1 = ~/Pictures/monitor1.jpg
name = "DP-1" output.DP-2 = ~/Pictures/monitor2.png
image = "~/Pictures/monitor1.jpg" output.HDMI-A-1 = ~/Pictures/hdmi_wallpaper.jpg
[[output]]
name = "DP-2"
image = "~/Pictures/monitor2.png"
[[output]]
name = "HDMI-A-1"
image = "~/Pictures/hdmi_wallpaper.jpg"
# You can also match outputs by description using the desc: prefix
# This is useful when output names change between reboots
# Format: output."desc:DESCRIPTION_PREFIX" = "/path/to/image.jpg"
[[output]]
name = "desc:Samsung"
image = "~/Pictures/samsung_wallpaper.jpg"
[[output]]
name = "desc:LG Ultra"
image = "~/Pictures/lg_wallpaper.jpg"
``` ```
### Finding Output Names ### Finding Output Names
@ -153,7 +134,7 @@ Options:
--version Show version information --version Show version information
Examples: Examples:
chroma -c ~/.config/chroma/chroma.toml chroma -c ~/.config/chroma/chroma.conf
chroma --daemon chroma --daemon
``` ```
@ -202,13 +183,10 @@ Chroma works with any Wayland compositor that supports:
- `wl_compositor` interface - `wl_compositor` interface
- `wl_output` interface - `wl_output` interface
- `zwlr_layer_shell_v1` (wlr-layer-shell-unstable-v1) interface
- EGL window surface creation - EGL window surface creation
The wlr-layer-shell protocol is required for creating wallpaper surfaces on the Tested only on Hyprland, but should work fine with any compositor that meets the
background layer. Tested only on Hyprland, but should work fine with any above criteria. Which is basically all of them I think?
wlroots-based compositor or any compositor that implements the wlr-layer-shell
protocol.
## Contributing ## Contributing

134
chroma.conf.sample Normal file
View file

@ -0,0 +1,134 @@
# This is a sample configuration file for the Chroma wallpaper daemon.
# Lines starting with # are comments and are ignored.
#
# Configuration file locations (checked in order):
# 1. ~/.config/chroma/chroma.conf
# 2. $XDG_CONFIG_HOME/chroma/chroma.conf
# 3. ./chroma.conf (current directory)
#
# To get started:
# 1. Copy this file to ~/.config/chroma/chroma.conf
# 2. Modify the paths to point to your wallpaper images
# 3. Use 'wlr-randr' or similar tools to find your output names
# 4. Restart chroma or send SIGHUP to reload configuration
# This image will be used for any output that doesn't have a specific mapping.
# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM
# Paths can be absolute or relative, ~ expansion is supported.
#
# Alternative examples:
# default_image = ~/Pictures/wallpapers/default.png
# default_image = /usr/share/wallpapers/default.jpg
# default_image = ./wallpapers/fallback.jpg
default_image = ~/.config/chroma/default.jpg
# Whether to run as a background daemon
# Usually set via command line option --daemon, but can be set here too
daemon_mode = false
# Global scaling mode for wallpapers (used as default for all outputs)
# Options: fill, fit, stretch, center
# fill - Fill entire output, crop if necessary (default)
# fit - Fit image within output, add borders if needed
# stretch - Stretch to fill output, may distort aspect ratio
# center - Center image at original size
scale_mode = fill
# Global image filtering quality (used as default for all outputs)
# Options: nearest, linear, bilinear, trilinear
# nearest - Nearest neighbor filtering (pixelated, fast)
# linear - Linear filtering (smooth, default)
# bilinear - Bilinear filtering (smoother)
# trilinear - Trilinear filtering (smoothest)
filter_quality = linear
# Default anchor position (0-100 for both x and y)
# anchor_x: 0=left, 50=center, 100=right
# anchor_y: 0=top, 50=center, 100=bottom
# Can use named anchors: center, top, bottom, left, right, top-left, etc.
anchor = center
anchor_x = 50
anchor_y = 50
# Image downsampling settings for performance optimization
# ========================================================
# Enable automatic downsampling of large images to save memory and improve performance
# Set to false to keep original resolution for all images (uses more memory!)
enable_downsampling = true
# Maximum expected output resolution
# Images larger than these dimensions may be automatically downsampled
# Adjust based on your actual monitor setup
max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p)
max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p)
# Minimum scale factor
# Prevents images from being scaled below this percentage of original size
# Useful to preserve detail on very high-resolution images
# Range: 0.1 to 1.0 (10% to 100%)
min_scale_factor = 0.25 # Don't scale below 25% of original size
# Output-specific wallpaper mappings
# ==================================
# Basic format: output.OUTPUT_NAME = /path/to/image.ext
#
# You can match outputs by name OR by description:
# output.DP-1 = /path/to/image.jpg # Match by port name
# output.desc:Samsung = /path/to/image.jpg # Match by description prefix
#
# The description is the human-readable name provided by the compositor
# via the Wayland wl_output description event. For example, if your
# monitor reports "Samsung T27A450" as its description, you can use
# "output.desc:Samsung" to match it. The match is a prefix match, so
# "output.desc:Sam" would also work.
#
# To find your output names and descriptions, run one of these commands:
# wlr-randr (for wlroots-based compositors)
# wayland-info | grep wl_output
# kanshi list-outputs
#
# Extended format with per-output settings:
# output.OUTPUT_NAME = /path/to/image.ext
# output.OUTPUT_NAME.scale = fill|fit|stretch|center
# output.OUTPUT_NAME.filter = nearest|linear|bilinear|trilinear
# output.OUTPUT_NAME.anchor = center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right
#
# Anchor specifies which part of the image is anchored to the output:
# center - image centered (default)
# top - image anchored to top edge
# bottom - image anchored to bottom edge
# left - image anchored to left edge
# right - image anchored to right edge
# top-left - image anchored to top-left corner
# top-right - image anchored to top-right corner
# bottom-left - image anchored to bottom-left corner
# bottom-right - image anchored to bottom-right corner
#
# Examples:
# output.HDMI-A-1 = ~/Pictures/wallpaper.jpg
# output.DP-1 = ~/Pictures/monitor1.png
# output.DP-1.scale = fit
# output.DP-1.filter = bilinear
# output.DP-1.anchor = top-left
# output.DP-2 = ~/Pictures/monitor2.jpg
# output.DP-2.scale = stretch
#
# # Match by monitor description (prefix match):
# output.desc:Samsung = ~/Pictures/samsung-wallpaper.jpg
# output.desc:Samsung.scale = fill
# output.desc:LG Ultra = ~/Pictures/lg-wallpaper.jpg
# output.desc:BenQ = ~/Pictures/benq-wallpaper.jpg
#
# # Laptop internal display:
# output.eDP-1 = ~/Pictures/laptop-wallpaper.jpg
# output.eDP-1.scale = fill
# output.eDP-1.anchor = bottom-right
# output.eDP-1.filter = trilinear
#
# Custom anchor coordinates (anchor_x, anchor_y override anchor):
# output.ULTRAWIDE = ~/Pictures/ultrawide.jpg
# output.ULTRAWIDE.scale = fill
# output.ULTRAWIDE.anchor_x = 25 # 25% from left edge
# output.ULTRAWIDE.anchor_y = 10 # 10% from top edge

View file

@ -1,127 +0,0 @@
# This is a sample TOML configuration file for the Chroma wallpaper daemon.
# For more information about TOML format, see https://toml.io
#
# Configuration file locations (checked in order):
# 1. $XDG_CONFIG_HOME/chroma/chroma.toml
# 2. ~/.config/chroma/chroma.toml
# 3. ./chroma.toml (current directory)
#
# To get started:
# 1. Copy this file to ~/.config/chroma/chroma.toml
# 2. Modify the paths to point to your wallpaper images
# 3. Use 'wlr-randr' or similar tools to find your output names
# 4. Restart chroma or send SIGHUP to reload configuration
# Default wallpaper for outputs without specific mapping
# Supports: JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC, PPM, PGM
# Paths can be absolute or relative, ~ expansion is supported.
default_image = "~/.config/chroma/default.jpg"
# Whether to run as a background daemon
# Usually set via command line option --daemon, but can be set here too
daemon_mode = false
# Global scaling mode for wallpapers (used as default for all outputs)
# Options: fill, fit, stretch, center
# fill - Fill entire output, crop if necessary (default)
# fit - Fit image within output, add borders if needed
# stretch - Stretch to fill output, may distort aspect ratio
# center - Center image at original size
scale_mode = "fill"
# Global image filtering quality (used as default for all outputs)
# Options: nearest, linear, bilinear, trilinear
# nearest - Nearest neighbor filtering (pixelated, fast)
# linear - Linear filtering (smooth, default)
# bilinear - Bilinear filtering (smoother)
# trilinear - Trilinear filtering (smoothest)
filter_quality = "linear"
# Default anchor position (0-100 for both x and y)
# anchor_x: 0=left, 50=center, 100=right
# anchor_y: 0=top, 50=center, 100=bottom
# Can use named anchors: center, top, bottom, left, right, top-left, etc.
anchor = "center"
anchor_x = 50
anchor_y = 50
# Image downsampling settings for performance optimization
[downsampling]
# Enable automatic downsampling of large images to save memory and improve performance
# Set to false to keep original resolution for all images (uses more memory!)
enable = true
# Maximum expected output resolution
# Images larger than these dimensions may be automatically downsampled
# Adjust based on your actual monitor setup
max_output_width = 3840 # 4K width (change to 2560 for 1440p, 1920 for 1080p)
max_output_height = 2160 # 4K height (change to 1440 for 1440p, 1080 for 1080p)
# Minimum scale factor
# Prevents images from being scaled below this percentage of original size
# Useful to preserve detail on very high-resolution images
# Range: 0.1 to 1.0 (10% to 100%)
min_scale_factor = 0.25 # Don't scale below 25% of original size
# Output-specific wallpaper mappings
# ==================================
# Each [[output]] block defines a mapping for a specific output.
#
# You can match outputs by name OR by description:
# name = "DP-1" # Match by port name
# name = "desc:Samsung" # Match by description prefix
#
# The description is the human-readable name provided by the compositor
# via the Wayland wl_output description event. For example, if your
# monitor reports "Samsung T27A450" as its description, you can use
# "desc:Samsung" to match it. The match is a prefix match, so
# "desc:Sam" would also work.
#
# To find your output names and descriptions, run one of these commands:
# wlr-randr (for wlroots-based compositors)
# wayland-info | grep wl_output
# kanshi list-outputs
[[output]]
name = "DP-1"
image = "~/Pictures/monitor1.jpg"
scale = "fill"
filter = "linear"
anchor = "center"
[[output]]
name = "DP-2"
image = "~/Pictures/monitor2.png"
scale = "fit"
[[output]]
name = "HDMI-A-1"
image = "~/Pictures/hdmi_wallpaper.jpg"
scale = "stretch"
# Match by monitor description (prefix match):
[[output]]
name = "desc:Samsung"
image = "~/Pictures/samsung-wallpaper.jpg"
scale = "fill"
[[output]]
name = "desc:LG Ultra"
image = "~/Pictures/lg-wallpaper.jpg"
scale = "fit"
# Laptop internal display:
[[output]]
name = "eDP-1"
image = "~/Pictures/laptop-wallpaper.jpg"
scale = "fill"
anchor = "bottom-right"
filter = "trilinear"
# Custom anchor coordinates (anchor_x, anchor_y override anchor):
[[output]]
name = "ULTRAWIDE"
image = "~/Pictures/ultrawide.jpg"
scale = "fill"
anchor_x = 25 # 25% from left edge
anchor_y = 10 # 10% from top edge

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1777312179, "lastModified": 1776074868,
"narHash": "sha256-AyQh4VtqwzVeLO1uHZ6/pzS6O96nXCXiUQZgCG+3X6k=", "narHash": "sha256-XwIXdLWyLhr+7rCjKBc0i54ExC6/s0XEC4jT8ozI2S4=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "20123554ae9affff1a5bc969ad3ad02e839f49cf", "rev": "fbb4cb95f81097fa79dae76545e2e7f6dc311f93",
"type": "github" "type": "github"
}, },
"original": { "original": {

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 <GLES2/gl2.h> #include <GL/gl.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.toml" #define CONFIG_FILE_NAME "chroma.conf"
// Log levels // Log levels
#define CHROMA_LOG_ERROR 0 #define CHROMA_LOG_ERROR 0
@ -73,7 +73,6 @@ 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
@ -117,7 +116,6 @@ 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
@ -165,9 +163,6 @@ typedef struct chroma_state {
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];
int output_count; int output_count;
@ -240,15 +235,12 @@ 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, int output_width, const chroma_config_t *config);
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, int output_width, const char *path);
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);
@ -256,7 +248,6 @@ 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

@ -2,7 +2,7 @@
#define CHROMA_VERSION_H #define CHROMA_VERSION_H
#ifndef CHROMA_VERSION #ifndef CHROMA_VERSION
#define CHROMA_VERSION "2.0.0" #define CHROMA_VERSION "1.1.0"
#endif #endif
#endif // CHROMA_VERSION_H #endif // CHROMA_VERSION_H

2915
include/vendor/tomlc17.c vendored

File diff suppressed because it is too large Load diff

View file

@ -1,190 +0,0 @@
/* Copyright (c) 2024-2026, CK Tan.
* https://github.com/cktan/tomlc17/blob/main/LICENSE
*/
#ifndef TOMLC17_H
#define TOMLC17_H
/*
* USAGE:
*
* 1. Call toml_parse(), toml_parse_file(), or toml_parse_file_ex()
* 2. Check result.ok
* 3. Use toml_get() or toml_seek() to query and traverse the
* result.toptab
* 4. Call toml_free() to release resources.
*
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#ifdef __cplusplus
#define TOML_EXTERN extern "C"
#else
#define TOML_EXTERN extern
#endif
enum toml_type_t {
TOML_UNKNOWN = 0,
TOML_STRING,
TOML_INT64,
TOML_FP64,
TOML_BOOLEAN,
TOML_DATE,
TOML_TIME,
TOML_DATETIME,
TOML_DATETIMETZ,
TOML_ARRAY,
TOML_TABLE,
};
typedef enum toml_type_t toml_type_t;
/* This is a Node in a Tree that represents a toml document rooted
* at toml_result_t::toptab.
*/
typedef struct toml_datum_t toml_datum_t;
struct toml_datum_t {
toml_type_t type;
uint32_t flag; // internal
union {
const char *s; // same as str.ptr; use if there are no NUL in string.
struct {
const char *ptr; // NUL terminated string
int len; // length excluding the terminating NUL.
} str;
int64_t int64; // integer
double fp64; // float
bool boolean;
struct { // date, time
int16_t year, month, day;
int16_t hour, minute, second;
int32_t usec;
int16_t tz; // in minutes
} ts;
struct { // array
int32_t size; // count elem
toml_datum_t *elem; // elem[]
} arr;
struct { // table
int32_t size; // count key
const char **key; // key[]
int *len; // len[]
toml_datum_t *value; // value[]
} tab;
} u;
};
/* Result returned by toml_parse() */
typedef struct toml_result_t toml_result_t;
struct toml_result_t {
bool ok; // success flag
toml_datum_t toptab; // valid if ok
char errmsg[200]; // valid if not ok
void *__internal; // do not use
};
/**
* Parse a toml document. Returns a toml_result which must be freed
* using toml_free() eventually.
*
* IMPORTANT: src[] must be a NUL terminated string! The len parameter
* does not include the NUL terminator.
*/
TOML_EXTERN toml_result_t toml_parse(const char *src, int len);
/**
* Parse a toml file. Returns a toml_result which must be freed
* using toml_free() eventually.
*
* IMPORTANT: you are still responsible to fclose(fp).
*/
TOML_EXTERN toml_result_t toml_parse_file(FILE *fp);
/**
* Parse a toml file. Returns a toml_result which must be freed
* using toml_free() eventually.
*/
TOML_EXTERN toml_result_t toml_parse_file_ex(const char *fname);
/**
* Release the result.
*/
TOML_EXTERN void toml_free(toml_result_t result);
/**
* Find a key in a toml_table. Return the value of the key if found,
* or a TOML_UNKNOWN otherwise.
*/
TOML_EXTERN toml_datum_t toml_get(toml_datum_t table, const char *key);
/**
* Locate a value starting from a toml_table. Return the value of the key if
* found, or a TOML_UNKNOWN otherwise.
*
* Note: the multipart-key is separated by DOT, and must not have any escape
* chars. The maximum length of the multipart_key must not exceed 127 bytes.
*/
TOML_EXTERN toml_datum_t toml_seek(toml_datum_t table,
const char *multipart_key);
/**
* OBSOLETE: use toml_get() instead.
* Find a key in a toml_table. Return the value of the key if found,
* or a TOML_UNKNOWN otherwise. (
*/
static inline toml_datum_t toml_table_find(toml_datum_t table,
const char *key) {
return toml_get(table, key);
}
/**
* Override values in r1 using r2. Return a new result. All results
* (i.e., r1, r2 and the returned result) must be freed using toml_free()
* after use.
*
* LOGIC:
* ret = copy of r1
* for each item x in r2:
* if x is not in ret:
* override
* elif x in ret is NOT of the same type:
* override
* elif x is an array of tables:
* append r2.x to ret.x
* elif x is a table:
* merge r2.x to ret.x
* else:
* override
*/
TOML_EXTERN toml_result_t toml_merge(const toml_result_t *r1,
const toml_result_t *r2);
/**
* Check if two results are the same. Dictionary and array orders are
* sensitive.
*/
TOML_EXTERN bool toml_equiv(const toml_result_t *r1, const toml_result_t *r2);
/* Options that override tomlc17 defaults globally */
typedef struct toml_option_t toml_option_t;
struct toml_option_t {
bool check_utf8; // Check all chars are valid utf8; default: false.
void *(*mem_realloc)(void *ptr, size_t size); // default: realloc()
void (*mem_free)(void *ptr); // default: free()
};
/**
* Get the default options. IF NECESSARY, use this to initialize
* toml_option_t and override values before calling
* toml_set_option().
*/
TOML_EXTERN toml_option_t toml_default_option(void);
/**
* Set toml options globally. Do this ONLY IF you are not satisfied with the
* defaults.
*/
TOML_EXTERN void toml_set_option(toml_option_t opt);
#endif // TOMLC17_H

View file

@ -15,7 +15,7 @@
in in
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "chroma"; pname = "chroma";
version = "2.0.0"; version = "1.1.0";
src = fs.toSource { src = fs.toSource {
root = s; root = s;
@ -64,7 +64,7 @@ in
''; '';
postInstall = '' postInstall = ''
install -Dm755 ${../chroma.toml.sample} $out/share/chroma.toml.sample install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample
''; '';
meta = { meta = {

View file

@ -5,9 +5,29 @@
#include <string.h> #include <string.h>
#include <strings.h> #include <strings.h>
#include "chroma.h" #include "../include/chroma.h"
#include "vendor/tomlc17.h"
static char *trim_whitespace(char *str) {
char *end;
// Trim leading whitespace
while (isspace((unsigned char)*str))
str++;
// All spaces?
if (*str == 0)
return str;
// Trim trailing whitespace
end = str + strlen(str) - 1;
while (end > str && isspace((unsigned char)*end))
end--;
*(end + 1) = '\0';
return str;
}
// Remove quotes from a string
// Match output name/description against a config pattern // Match output name/description against a config pattern
// Supports: // Supports:
// - Exact name match: "DP-1" matches wl_output.name == "DP-1" // - Exact name match: "DP-1" matches wl_output.name == "DP-1"
@ -40,8 +60,31 @@ static bool match_output(const char *pattern, const char *output_name,
return false; return false;
} }
static char *remove_quotes(char *str) {
size_t len = strlen(str);
if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
(str[0] == '\'' && str[len - 1] == '\''))) {
str[len - 1] = '\0';
return str + 1;
}
return str;
}
// Parse boolean value from string
static bool parse_bool(const char *value) {
if (!value)
return false;
if (strcasecmp(value, "true") == 0 || strcasecmp(value, "yes") == 0 ||
strcasecmp(value, "1") == 0 || strcasecmp(value, "on") == 0) {
return true;
}
return false;
}
// Parse scaling mode from string // Parse scaling mode from string
chroma_scale_mode_t parse_scale_mode(const char *value) { static chroma_scale_mode_t parse_scale_mode(const char *value) {
if (!value) if (!value)
return CHROMA_SCALE_FILL; // default return CHROMA_SCALE_FILL; // default
@ -60,7 +103,7 @@ chroma_scale_mode_t parse_scale_mode(const char *value) {
} }
// Parse filter quality from string // Parse filter quality from string
chroma_filter_quality_t parse_filter_quality(const char *value) { static chroma_filter_quality_t parse_filter_quality(const char *value) {
if (!value) if (!value)
return CHROMA_FILTER_LINEAR; // default return CHROMA_FILTER_LINEAR; // default
@ -111,7 +154,7 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
} }
// Parse anchor position from string // Parse anchor position from string
chroma_anchor_t parse_anchor(const char *value) { static chroma_anchor_t parse_anchor(const char *value) {
if (!value) if (!value)
return CHROMA_ANCHOR_CENTER; return CHROMA_ANCHOR_CENTER;
@ -170,10 +213,12 @@ static const char *anchor_to_string(chroma_anchor_t anchor) {
} }
// Output-to-image mapping // Output-to-image mapping
int add_output_mapping(chroma_config_t *config, const char *output_name, static int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path, chroma_scale_mode_t scale_mode, const char *image_path,
chroma_scale_mode_t scale_mode,
chroma_filter_quality_t filter_quality, chroma_filter_quality_t filter_quality,
chroma_anchor_t anchor, float anchor_x, float anchor_y) { chroma_anchor_t anchor, float anchor_x,
float anchor_y) {
if (!config || !output_name || !image_path) { if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
} }
@ -223,194 +268,6 @@ int add_output_mapping(chroma_config_t *config, const char *output_name,
return CHROMA_OK; return CHROMA_OK;
} }
// Parse TOML configuration file
int chroma_config_load_toml(chroma_config_t *config, const char *config_file) {
if (!config || !config_file) {
return CHROMA_ERROR_INIT;
}
toml_result_t result = toml_parse_file_ex(config_file);
if (!result.ok) {
chroma_log("DEBUG", "TOML parse failed: %s", result.errmsg);
toml_free(result);
return CHROMA_ERROR_CONFIG;
}
chroma_log("INFO", "Loading TOML configuration from: %s", config_file);
// Parse default_image
toml_datum_t default_image = toml_seek(result.toptab, "default_image");
if (default_image.type == TOML_STRING) {
char *expanded_path = chroma_expand_path(default_image.u.s);
const char *path_to_use = expanded_path ? expanded_path : default_image.u.s;
if (strlen(path_to_use) < sizeof(config->default_image)) {
strcpy(config->default_image, path_to_use);
} else {
chroma_log("WARN",
"default_image path too long (%zu >= %zu), truncating: %s",
strlen(path_to_use), sizeof(config->default_image), path_to_use);
snprintf(config->default_image, sizeof(config->default_image), "%s",
path_to_use);
}
if (expanded_path) {
free(expanded_path);
}
}
// Parse daemon_mode
toml_datum_t daemon_mode = toml_seek(result.toptab, "daemon_mode");
if (daemon_mode.type == TOML_BOOLEAN) {
config->daemon_mode = daemon_mode.u.boolean;
}
// Parse scale_mode
toml_datum_t scale_mode = toml_seek(result.toptab, "scale_mode");
if (scale_mode.type == TOML_STRING) {
config->default_scale_mode = parse_scale_mode(scale_mode.u.s);
}
// Parse filter_quality
toml_datum_t filter_quality = toml_seek(result.toptab, "filter_quality");
if (filter_quality.type == TOML_STRING) {
config->default_filter_quality = parse_filter_quality(filter_quality.u.s);
}
// Parse anchor
toml_datum_t anchor = toml_seek(result.toptab, "anchor");
if (anchor.type == TOML_STRING) {
config->default_anchor = parse_anchor(anchor.u.s);
}
// Parse anchor_x
toml_datum_t anchor_x = toml_seek(result.toptab, "anchor_x");
if (anchor_x.type == TOML_INT64) {
config->default_anchor_x = (float)anchor_x.u.int64;
} else if (anchor_x.type == TOML_FP64) {
config->default_anchor_x = (float)anchor_x.u.fp64;
}
// Parse anchor_y
toml_datum_t anchor_y = toml_seek(result.toptab, "anchor_y");
if (anchor_y.type == TOML_INT64) {
config->default_anchor_y = (float)anchor_y.u.int64;
} else if (anchor_y.type == TOML_FP64) {
config->default_anchor_y = (float)anchor_y.u.fp64;
}
// Parse downsampling section
toml_datum_t downsampling = toml_seek(result.toptab, "downsampling");
if (downsampling.type == TOML_TABLE) {
toml_datum_t ds_tab = downsampling;
toml_datum_t enable = toml_seek(ds_tab, "enable");
if (enable.type == TOML_BOOLEAN) {
config->enable_downsampling = enable.u.boolean;
}
toml_datum_t max_width = toml_seek(ds_tab, "max_output_width");
if (max_width.type == TOML_INT64) {
config->max_output_width = (int)max_width.u.int64;
}
toml_datum_t max_height = toml_seek(ds_tab, "max_output_height");
if (max_height.type == TOML_INT64) {
config->max_output_height = (int)max_height.u.int64;
}
toml_datum_t min_scale = toml_seek(ds_tab, "min_scale_factor");
if (min_scale.type == TOML_FP64) {
config->min_scale_factor = (float)min_scale.u.fp64;
} else if (min_scale.type == TOML_INT64) {
config->min_scale_factor = (float)min_scale.u.int64;
}
}
// Parse output mappings array
toml_datum_t outputs = toml_seek(result.toptab, "output");
if (outputs.type == TOML_ARRAY) {
for (int i = 0; i < outputs.u.arr.size; i++) {
toml_datum_t output = outputs.u.arr.elem[i];
if (output.type != TOML_TABLE) {
continue;
}
toml_datum_t output_tab = output;
// Get output name
toml_datum_t name = toml_seek(output_tab, "name");
if (name.type != TOML_STRING) {
chroma_log("WARN", "Output mapping %d missing name", i);
continue;
}
// Get image path
toml_datum_t image = toml_seek(output_tab, "image");
if (image.type != TOML_STRING) {
chroma_log("WARN", "Output mapping %d missing image", i);
continue;
}
char *expanded_path = chroma_expand_path(image.u.s);
const char *path_to_use = expanded_path ? expanded_path : image.u.s;
// Get optional settings with defaults from global config
chroma_scale_mode_t out_scale = config->default_scale_mode;
chroma_filter_quality_t out_filter = config->default_filter_quality;
chroma_anchor_t out_anchor = config->default_anchor;
float out_anchor_x = config->default_anchor_x;
float out_anchor_y = config->default_anchor_y;
toml_datum_t out_scale_val = toml_seek(output_tab, "scale");
if (out_scale_val.type == TOML_STRING) {
out_scale = parse_scale_mode(out_scale_val.u.s);
}
toml_datum_t out_filter_val = toml_seek(output_tab, "filter");
if (out_filter_val.type == TOML_STRING) {
out_filter = parse_filter_quality(out_filter_val.u.s);
}
toml_datum_t out_anchor_val = toml_seek(output_tab, "anchor");
if (out_anchor_val.type == TOML_STRING) {
out_anchor = parse_anchor(out_anchor_val.u.s);
}
toml_datum_t out_anchor_x_val = toml_seek(output_tab, "anchor_x");
if (out_anchor_x_val.type == TOML_INT64) {
out_anchor_x = (float)out_anchor_x_val.u.int64;
} else if (out_anchor_x_val.type == TOML_FP64) {
out_anchor_x = (float)out_anchor_x_val.u.fp64;
}
toml_datum_t out_anchor_y_val = toml_seek(output_tab, "anchor_y");
if (out_anchor_y_val.type == TOML_INT64) {
out_anchor_y = (float)out_anchor_y_val.u.int64;
} else if (out_anchor_y_val.type == TOML_FP64) {
out_anchor_y = (float)out_anchor_y_val.u.fp64;
}
// Add the mapping
if (add_output_mapping(config, name.u.s, path_to_use, out_scale,
out_filter, out_anchor, out_anchor_x,
out_anchor_y) != CHROMA_OK) {
chroma_log("ERROR", "Failed to add TOML output mapping: %s", name.u.s);
}
if (expanded_path) {
free(expanded_path);
}
}
}
toml_free(result);
chroma_log("INFO",
"Loaded TOML configuration: %d output mappings, default image: %s",
config->mapping_count, config->default_image);
return CHROMA_OK;
}
// Initialize configuration with defaults // Initialize configuration with defaults
static void init_default_config(chroma_config_t *config) { static void init_default_config(chroma_config_t *config) {
if (!config) if (!config)
@ -434,12 +291,313 @@ static void init_default_config(chroma_config_t *config) {
config->max_output_height = 2160; // 4K height config->max_output_height = 2160; // 4K height
config->min_scale_factor = 0.25f; // don't scale below 25% config->min_scale_factor = 0.25f; // don't scale below 25%
// Leave default_image empty - user must configure it explicitly // Set default image path (can be overridden)
// This avoids errors when the hardcoded path doesn't exist const char *home = getenv("HOME");
config->default_image[0] = '\0'; if (home) {
snprintf(config->default_image, sizeof(config->default_image),
"%s/.config/chroma/default.jpg", home);
} else {
strcpy(config->default_image, "/usr/share/pixmaps/chroma-default.jpg");
}
} }
// Load configuration from file (TOML format only) // Parse a single configuration line
static int parse_config_line(chroma_config_t *config, char *line,
int line_number) {
if (!config || !line) {
return CHROMA_ERROR_INIT;
}
// Skip empty lines and comments
char *trimmed = trim_whitespace(line);
if (*trimmed == '\0' || *trimmed == '#' || *trimmed == ';') {
return CHROMA_OK;
}
// Find the equals sign
char *equals = strchr(trimmed, '=');
if (!equals) {
chroma_log("WARN", "Invalid config line %d: no '=' found", line_number);
return CHROMA_OK; // continue parsing
}
*equals = '\0';
char *key = trim_whitespace(trimmed);
char *value = trim_whitespace(equals + 1);
value = remove_quotes(value);
if (*key == '\0' || *value == '\0') {
chroma_log("WARN", "Invalid config line %d: empty key or value",
line_number);
return CHROMA_OK;
}
// Parse configuration options
if (strcasecmp(key, "default_image") == 0) {
char *expanded_path = chroma_expand_path(value);
const char *path_to_use = expanded_path ? expanded_path : value;
size_t path_len = strlen(path_to_use);
if (path_len >= sizeof(config->default_image)) {
chroma_log("ERROR", "Default image path too long: %s (max %zu)",
path_to_use, sizeof(config->default_image) - 1);
if (expanded_path) {
free(expanded_path);
}
return CHROMA_ERROR_CONFIG;
}
strcpy(config->default_image, path_to_use);
if (expanded_path) {
chroma_log("DEBUG", "Set default image: %s -> %s", value, expanded_path);
chroma_log("TRACE", "Default image path set: length=%zu, expanded='%s'",
path_len, expanded_path);
free(expanded_path);
} else {
chroma_log("WARN", "Failed to expand path, using original: %s", value);
}
} else if (strcasecmp(key, "daemon") == 0 ||
strcasecmp(key, "daemon_mode") == 0) {
config->daemon_mode = parse_bool(value);
chroma_log("DEBUG", "Set daemon mode: %s",
config->daemon_mode ? "true" : "false");
chroma_log("TRACE",
"Daemon mode configuration: key='%s', value='%s', parsed=%s",
key, value, config->daemon_mode ? "true" : "false");
} else if (strcasecmp(key, "scale_mode") == 0 ||
strcasecmp(key, "default_scale_mode") == 0) {
config->default_scale_mode = parse_scale_mode(value);
chroma_log("DEBUG", "Set default scale mode: %s",
scale_mode_to_string(config->default_scale_mode));
chroma_log("TRACE",
"Scale mode configuration: key='%s', value='%s', parsed=%s", key,
value, scale_mode_to_string(config->default_scale_mode));
} else if (strcasecmp(key, "filter_quality") == 0 ||
strcasecmp(key, "default_filter_quality") == 0) {
config->default_filter_quality = parse_filter_quality(value);
chroma_log("DEBUG", "Set default filter quality: %s",
filter_quality_to_string(config->default_filter_quality));
chroma_log("TRACE",
"Filter quality configuration: key='%s', value='%s', parsed=%s",
key, value,
filter_quality_to_string(config->default_filter_quality));
} else if (strcasecmp(key, "enable_downsampling") == 0) {
config->enable_downsampling = parse_bool(value);
chroma_log("DEBUG", "Set downsampling: %s",
config->enable_downsampling ? "enabled" : "disabled");
} else if (strcasecmp(key, "anchor") == 0) {
config->default_anchor = parse_anchor(value);
chroma_log("DEBUG", "Set default anchor: %s",
anchor_to_string(config->default_anchor));
} else if (strcasecmp(key, "anchor_x") == 0) {
char *endptr = NULL;
float ax = strtof(value, &endptr);
if (endptr == value || *endptr != '\0') {
chroma_log("WARN", "Invalid anchor_x: %s (not a number, using 50)",
value);
config->default_anchor_x = 50.0f;
} else if (ax < 0.0f || ax > 100.0f) {
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)", value);
config->default_anchor_x = 50.0f;
} else {
config->default_anchor_x = ax;
chroma_log("DEBUG", "Set default anchor_x: %.1f", (double)ax);
}
} else if (strcasecmp(key, "anchor_y") == 0) {
char *endptr = NULL;
float ay = strtof(value, &endptr);
if (endptr == value || *endptr != '\0') {
chroma_log("WARN", "Invalid anchor_y: %s (not a number, using 50)",
value);
config->default_anchor_y = 50.0f;
} else if (ay < 0.0f || ay > 100.0f) {
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)", value);
config->default_anchor_y = 50.0f;
} else {
config->default_anchor_y = ay;
chroma_log("DEBUG", "Set default anchor_y: %.1f", (double)ay);
}
} else if (strcasecmp(key, "max_output_width") == 0) {
int width = atoi(value);
if (width > 0 && width <= 16384) { // Reasonable limits
config->max_output_width = width;
chroma_log("DEBUG", "Set max output width: %d", width);
} else {
chroma_log("WARN", "Invalid max_output_width: %s (using 3840)", value);
}
} else if (strcasecmp(key, "max_output_height") == 0) {
int height = atoi(value);
if (height > 0 && height <= 16384) { // Reasonable limits
config->max_output_height = height;
chroma_log("DEBUG", "Set max output height: %d", height);
} else {
chroma_log("WARN", "Invalid max_output_height: %s (using 2160)", value);
}
} else if (strcasecmp(key, "min_scale_factor") == 0) {
float factor = (float)atof(value);
if (factor > 0.0f && factor <= 1.0f) {
config->min_scale_factor = factor;
chroma_log("DEBUG", "Set minimum scale factor: %.2f", (double)factor);
} else {
chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
}
} else if (strncasecmp(key, "output.", 7) == 0) {
// Output-specific mapping: e.g., output.DP-1=/path/to/image.jpg
const char *output_name = key + 7;
if (*output_name == '\0') {
chroma_log("WARN", "Invalid output mapping line %d: no output name",
line_number);
return CHROMA_OK;
}
// Check for extended output configuration with properties. Format:
// output.DP-1.scale = fill
// output.DP-1.filter = linear
char *dot = strchr(output_name, '.');
if (dot) {
// This is an output property (scale or filter)
*dot = '\0';
const char *property = dot + 1;
// Find existing mapping for this output
chroma_config_mapping_t *mapping = NULL;
for (int i = 0; i < config->mapping_count; i++) {
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
mapping = &config->mappings[i];
break;
}
}
if (!mapping) {
chroma_log("WARN", "Output %s not found for property %s (line %d)",
output_name, property, line_number);
return CHROMA_OK;
}
if (strcasecmp(property, "scale") == 0) {
mapping->scale_mode = parse_scale_mode(value);
chroma_log("DEBUG", "Set scale mode for output %s: %s", output_name,
scale_mode_to_string(mapping->scale_mode));
} else if (strcasecmp(property, "filter") == 0) {
mapping->filter_quality = parse_filter_quality(value);
chroma_log("DEBUG", "Set filter quality for output %s: %s", output_name,
filter_quality_to_string(mapping->filter_quality));
} else if (strcasecmp(property, "anchor") == 0) {
mapping->anchor = parse_anchor(value);
// Set anchor_x/anchor_y based on named anchor
switch (mapping->anchor) {
case CHROMA_ANCHOR_TOP:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_BOTTOM:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 100.0f;
break;
case CHROMA_ANCHOR_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 50.0f;
break;
case CHROMA_ANCHOR_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 50.0f;
break;
case CHROMA_ANCHOR_TOP_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_TOP_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 0.0f;
break;
case CHROMA_ANCHOR_BOTTOM_LEFT:
mapping->anchor_x = 0.0f;
mapping->anchor_y = 100.0f;
break;
case CHROMA_ANCHOR_BOTTOM_RIGHT:
mapping->anchor_x = 100.0f;
mapping->anchor_y = 100.0f;
break;
default:
mapping->anchor_x = 50.0f;
mapping->anchor_y = 50.0f;
break;
}
chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",
output_name, anchor_to_string(mapping->anchor),
(double)mapping->anchor_x, (double)mapping->anchor_y);
} else if (strcasecmp(property, "anchor_x") == 0) {
float ax = (float)atof(value);
if (ax >= 0.0f && ax <= 100.0f) {
mapping->anchor_x = ax;
chroma_log("DEBUG", "Set anchor_x for output %s: %.1f", output_name,
(double)ax);
} else {
mapping->anchor_x = 50.0f;
chroma_log("WARN", "Invalid anchor_x: %s (range 0-100, using 50)",
value);
}
} else if (strcasecmp(property, "anchor_y") == 0) {
float ay = (float)atof(value);
if (ay >= 0.0f && ay <= 100.0f) {
mapping->anchor_y = ay;
chroma_log("DEBUG", "Set anchor_y for output %s: %.1f", output_name,
(double)ay);
} else {
mapping->anchor_y = 50.0f;
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",
value);
}
} else {
chroma_log("WARN", "Unknown output property: %s (line %d)", property,
line_number);
}
return CHROMA_OK;
}
// Expand path before validation and storage
char *expanded_path = chroma_expand_path(value);
const char *path_to_validate = expanded_path ? expanded_path : value;
// Validate image path
if (chroma_image_validate(path_to_validate) != CHROMA_OK) {
chroma_log("WARN", "Invalid image path for output %s: %s (expanded: %s)",
output_name, value, path_to_validate);
if (expanded_path) {
free(expanded_path);
}
return CHROMA_OK;
}
if (add_output_mapping(
config, output_name, path_to_validate, config->default_scale_mode,
config->default_filter_quality, config->default_anchor,
config->default_anchor_x, config->default_anchor_y) != CHROMA_OK) {
chroma_log("ERROR", "Failed to add output mapping: %s -> %s", output_name,
path_to_validate);
if (expanded_path) {
free(expanded_path);
}
return CHROMA_ERROR_CONFIG;
}
if (expanded_path) {
free(expanded_path);
}
} else {
chroma_log("WARN", "Unknown configuration key line %d: %s", line_number,
key);
chroma_log("TRACE", "Unrecognized config line %d: key='%s', value='%s'",
line_number, key, value);
}
return CHROMA_OK;
}
// Load configuration from file
int chroma_config_load(chroma_config_t *config, const char *config_file) { int chroma_config_load(chroma_config_t *config, const char *config_file) {
if (!config) { if (!config) {
return CHROMA_ERROR_INIT; return CHROMA_ERROR_INIT;
@ -453,20 +611,52 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) {
return CHROMA_OK; return CHROMA_OK;
} }
// Check if file exists FILE *file = fopen(config_file, "r");
if (!chroma_path_exists(config_file)) { if (!file) {
if (errno == ENOENT) {
chroma_log("INFO", "Config file not found: %s (using defaults)", chroma_log("INFO", "Config file not found: %s (using defaults)",
config_file); config_file);
return CHROMA_OK; return CHROMA_OK;
} else {
chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
strerror(errno));
return CHROMA_ERROR_CONFIG;
}
} }
// Load TOML configuration chroma_log("INFO", "Loading configuration from: %s", config_file);
int result = chroma_config_load_toml(config, config_file); chroma_log("TRACE",
if (result != CHROMA_OK) { "Starting configuration parsing, estimated config size: %ld bytes",
chroma_log("ERROR", "Failed to load TOML config from %s", config_file); chroma_get_file_size(config_file));
return result;
char line[1024];
int line_number = 0;
int parse_errors = 0;
while (fgets(line, sizeof(line), file)) {
line_number++;
char *newline = strchr(line, '\n');
if (newline) {
*newline = '\0';
} }
if (parse_config_line(config, line, line_number) != CHROMA_OK) {
parse_errors++;
}
}
fclose(file);
if (parse_errors > 0) {
chroma_log("WARN", "Config file contained %d errors", parse_errors);
// Continue anyway with partial configuration
}
chroma_log("INFO",
"Loaded configuration: %d output mappings, default image: %s",
config->mapping_count, config->default_image);
// Log configuration memory usage // Log configuration memory usage
size_t config_size = size_t config_size =
sizeof(chroma_config_t) + sizeof(chroma_config_t) +

View file

@ -85,19 +85,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
return CHROMA_ERROR_CONFIG; return CHROMA_ERROR_CONFIG;
} }
// Check if image path is empty (no default configured) // Load or get cached image
if (strlen(image_path) == 0) { chroma_image_t *image = chroma_image_get_or_load(state, image_path);
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);
@ -107,7 +96,6 @@ 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",
@ -173,8 +161,7 @@ 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: %s", output->id, chroma_log("ERROR", "Failed to render wallpaper for output %u", output->id);
chroma_error_string(ret));
return ret; return ret;
} }

View file

@ -58,9 +58,8 @@ 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, int src_channels, int src_height, unsigned char *dst_data,
unsigned char *dst_data, int dst_width, int dst_width, int dst_height) {
int dst_height, int dst_channels) {
if (!src_data || !dst_data) { if (!src_data || !dst_data) {
return -1; return -1;
} }
@ -78,18 +77,14 @@ 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 data // Copy pixel (RGBA)
int src_idx = (src_y * src_width + src_x) * src_channels; int src_idx = (src_y * src_width + src_x) * 4;
int dst_idx = (y * dst_width + x) * dst_channels; int dst_idx = (y * dst_width + x) * 4;
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
if (dst_channels == 4 && src_channels == 4) {
dst_data[dst_idx + 3] = src_data[src_idx + 3]; // A 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
}
} }
} }
@ -97,11 +92,8 @@ 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, int output_width, const chroma_config_t *config) {
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;
@ -125,24 +117,12 @@ 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 // Load image data using stb_image, force RGBA format to avoid conversion
// 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
int actual_channels = 0; image->data =
if (!stbi_info(path, &image->width, &image->height, &actual_channels)) { stbi_load(path, &image->width, &image->height, &image->channels, 4);
chroma_log("ERROR", "Failed to get image info for %s: %s", path, image->channels = 4; // always RGBA after forced conversion
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());
@ -157,8 +137,13 @@ int chroma_image_load(chroma_image_t *image, const char *path,
return CHROMA_ERROR_IMAGE; return CHROMA_ERROR_IMAGE;
} }
chroma_log("DEBUG", "Loaded image %s with %d channels (original had %d)", // Validate we have RGBA data (should always be true with forced conversion)
path, image->channels, actual_channels); 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;
}
// Store original dimensions before potential downsampling // Store original dimensions before potential downsampling
int original_width = image->width; int original_width = image->width;
@ -170,14 +155,9 @@ 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) {
// Use output dimensions if provided, otherwise fall back to config defaults calculate_optimal_size(original_width, original_height,
int max_width = config->max_output_width, config->max_output_height,
(output_width > 0) ? output_width : config->max_output_width; &optimal_width, &optimal_height);
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;
@ -198,7 +178,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);
} }
// Downsample if needed and enabled // Downsamp 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;
@ -207,8 +187,7 @@ 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_size = (size_t)optimal_width * (size_t)optimal_height * 4;
(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");
@ -217,8 +196,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,
image->channels, downsampled_data, optimal_width, downsampled_data, optimal_width,
optimal_height, image->channels) != 0) { optimal_height) != 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);
@ -239,7 +218,6 @@ 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 =
@ -280,7 +258,6 @@ 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);
@ -289,18 +266,6 @@ 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) {
@ -318,11 +283,8 @@ 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, int output_width, const char *path) {
int output_height) {
if (!state || !path) { if (!state || !path) {
return NULL; return NULL;
} }
@ -330,9 +292,7 @@ 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 (ref_count: %d)", path, chroma_log("DEBUG", "Using cached image: %s", path);
existing->ref_count);
existing->ref_count++;
return existing; return existing;
} }
@ -347,9 +307,8 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
state->image_count++; state->image_count++;
} }
// Load the image with configuration and output dimensions // Load the image with configuration
if (chroma_image_load(image, path, &state->config, output_width, if (chroma_image_load(image, path, &state->config) != CHROMA_OK) {
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--;
@ -417,7 +376,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_release(&state->images[i]); chroma_image_free(&state->images[i]);
} }
state->image_count = 0; state->image_count = 0;

View file

@ -24,7 +24,7 @@ static void print_usage(const char *program_name) {
printf(" -h, --help Show this help\n"); printf(" -h, --help Show this help\n");
printf(" --version Show version information\n"); printf(" --version Show version information\n");
printf("\nExamples:\n"); printf("\nExamples:\n");
printf(" %s -c ~/.config/chroma/chroma.toml\n", program_name); printf(" %s -c ~/.config/chroma/chroma.conf\n", program_name);
printf(" %s --daemon\n", program_name); printf(" %s --daemon\n", program_name);
} }

View file

@ -187,6 +187,7 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
// Vertex shader for simple texture rendering // Vertex shader for simple texture rendering
static const char *vertex_shader_source = static const char *vertex_shader_source =
"#version 120\n"
"attribute vec2 position;\n" "attribute vec2 position;\n"
"attribute vec2 texcoord;\n" "attribute vec2 texcoord;\n"
"varying vec2 v_texcoord;\n" "varying vec2 v_texcoord;\n"
@ -197,7 +198,7 @@ static const char *vertex_shader_source =
// Fragment shader for simple texture rendering // Fragment shader for simple texture rendering
static const char *fragment_shader_source = static const char *fragment_shader_source =
"precision mediump float;\n" "#version 120\n"
"varying vec2 v_texcoord;\n" "varying vec2 v_texcoord;\n"
"uniform sampler2D texture;\n" "uniform sampler2D texture;\n"
"void main() {\n" "void main() {\n"
@ -213,7 +214,7 @@ static const float vertices[] = {
-1.0f, 1.0f, 0.0f, 0.0f, // top left -1.0f, 1.0f, 0.0f, 0.0f, // top left
}; };
static const unsigned short indices[] = { static const unsigned int indices[] = {
0, 1, 2, // first triangle 0, 1, 2, // first triangle
2, 3, 0 // second triangle 2, 3, 0 // second triangle
}; };
@ -283,30 +284,18 @@ static GLuint create_shader_program(void) {
} }
// Initialize OpenGL resources for output // Initialize OpenGL resources for output
static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) { static int init_gl_resources(chroma_output_t *output) {
if (!output || output->gl_resources_initialized) { if (!output || output->gl_resources_initialized) {
return CHROMA_OK; return CHROMA_OK;
} }
// Use shared shader program from state if available // Create shader prog
if (state && state->shader_program != 0) {
output->shader_program = state->shader_program;
chroma_log("DEBUG", "Using shared shader program for output %u",
output->id);
} else {
// Create shader program
output->shader_program = create_shader_program(); output->shader_program = create_shader_program();
if (!output->shader_program) { if (!output->shader_program) {
chroma_log("ERROR", "Failed to create shader program for output %u", chroma_log("ERROR", "Failed to create shader program for output %u",
output->id); output->id);
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
// Store in state for sharing
if (state) {
state->shader_program = output->shader_program;
chroma_log("DEBUG", "Created shared shader program");
}
}
// Create and setup VBO/EBO // Create and setup VBO/EBO
glGenBuffers(1, &output->vbo); glGenBuffers(1, &output->vbo);
@ -319,10 +308,6 @@ static int init_gl_resources(chroma_state_t *state, chroma_output_t *output) {
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices,
GL_STATIC_DRAW); GL_STATIC_DRAW);
// Unbind buffers
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
output->texture_id = 0; // will be created when image is assigned output->texture_id = 0; // will be created when image is assigned
output->vbo_dirty = true; // VBO needs initial update output->vbo_dirty = true; // VBO needs initial update
output->gl_resources_initialized = true; output->gl_resources_initialized = true;
@ -385,24 +370,9 @@ static int update_texture_from_image(chroma_output_t *output,
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
// Upload texture data with appropriate format based on channels // Upload texture data (always RGBA now)
GLint internal_format; glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
GLenum format; GL_RGBA, GL_UNSIGNED_BYTE, image->data);
if (image->channels == 4) {
internal_format = GL_RGBA;
format = GL_RGBA;
} else if (image->channels == 3) {
internal_format = GL_RGB;
format = GL_RGB;
} else if (image->channels == 2) {
internal_format = GL_LUMINANCE_ALPHA;
format = GL_LUMINANCE_ALPHA;
} else {
internal_format = GL_LUMINANCE;
format = GL_LUMINANCE;
}
glTexImage2D(GL_TEXTURE_2D, 0, internal_format, image->width, image->height,
0, format, GL_UNSIGNED_BYTE, image->data);
// Generate mipmaps for trilinear filtering if they're needed // Generate mipmaps for trilinear filtering if they're needed
if (needs_mipmaps) { if (needs_mipmaps) {
@ -415,24 +385,38 @@ static int update_texture_from_image(chroma_output_t *output,
// Mark this output as having uploaded its texture // Mark this output as having uploaded its texture
output->texture_uploaded = true; output->texture_uploaded = true;
// Decrement reference count and free system RAM when all outputs // Free system RAM copy only when ALL outputs using this image have uploaded
// have uploaded their textures // to GPU
image->ref_count--; if (image->data) {
if (image->ref_count <= 0 && image->data) { // Count total outputs using this image and how many have uploaded
size_t freed_bytes = int total_using = 0;
(size_t)image->width * (size_t)image->height * (size_t)image->channels; 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 * (size_t)image->height *
(size_t)image->channels;
stbi_image_free(image->data); stbi_image_free(image->data);
image->data = NULL; image->data = NULL;
chroma_log("INFO", "Freed %.2f MB of image data after GPU upload: %s", chroma_log("INFO",
(double)freed_bytes / (1024.0 * 1024.0), image->path); "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_resource_deallocation("image_data", freed_bytes, chroma_log_resource_deallocation("image_data", freed_bytes,
"post-gpu-upload"); "post-gpu-upload");
chroma_log_memory_stats("post-gpu-upload"); chroma_log_memory_stats("post-gpu-upload");
} else { }
chroma_log("DEBUG",
"Keeping image data for %s (ref_count: %d, waiting for %d more "
"outputs)",
image->path, image->ref_count, image->ref_count);
} }
chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id, chroma_log("DEBUG", "Updated texture for output %u (%dx%d)", output->id,
@ -441,8 +425,7 @@ static int update_texture_from_image(chroma_output_t *output,
} }
// Cleanup OpenGL resources for output // Cleanup OpenGL resources for output
static void cleanup_gl_resources(chroma_state_t *state, static void cleanup_gl_resources(chroma_output_t *output) {
chroma_output_t *output) {
if (!output || !output->gl_resources_initialized) { if (!output || !output->gl_resources_initialized) {
return; return;
} }
@ -453,12 +436,8 @@ static void cleanup_gl_resources(chroma_state_t *state,
output->texture_id = 0; output->texture_id = 0;
} }
// Don't delete shared shader program here, it's managed by state
if (output->shader_program != 0) { if (output->shader_program != 0) {
// Only delete if this output owns the program (not shared)
if (state && state->shader_program != output->shader_program) {
glDeleteProgram(output->shader_program); glDeleteProgram(output->shader_program);
}
output->shader_program = 0; output->shader_program = 0;
} }
@ -487,9 +466,9 @@ static int choose_egl_config(EGLDisplay display, EGLConfig *config) {
EGL_BLUE_SIZE, EGL_BLUE_SIZE,
8, 8,
EGL_ALPHA_SIZE, EGL_ALPHA_SIZE,
0, 8,
EGL_RENDERABLE_TYPE, EGL_RENDERABLE_TYPE,
EGL_OPENGL_ES2_BIT, EGL_OPENGL_BIT,
EGL_NONE}; EGL_NONE};
EGLint num_configs; EGLint num_configs;
@ -529,9 +508,9 @@ int chroma_egl_init(chroma_state_t *state) {
chroma_log("INFO", "EGL initialized: version %d.%d", major, minor); chroma_log("INFO", "EGL initialized: version %d.%d", major, minor);
chroma_log_memory_stats("post-egl-init"); chroma_log_memory_stats("post-egl-init");
// Bind OpenGL ES API for better efficiency on embedded/GPU drivers // Bind OpenGL API
if (!eglBindAPI(EGL_OPENGL_ES_API)) { if (!eglBindAPI(EGL_OPENGL_API)) {
chroma_log("ERROR", "Failed to bind OpenGL ES API: 0x%04x", eglGetError()); chroma_log("ERROR", "Failed to bind OpenGL API: 0x%04x", eglGetError());
chroma_egl_cleanup(state); chroma_egl_cleanup(state);
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
@ -542,8 +521,9 @@ int chroma_egl_init(chroma_state_t *state) {
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
// Create EGL context for GLES 2.0 // Create EGL context
EGLint context_attributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE}; EGLint context_attributes[] = {EGL_CONTEXT_MAJOR_VERSION, 2,
EGL_CONTEXT_MINOR_VERSION, 1, EGL_NONE};
state->egl_context = eglCreateContext(state->egl_display, state->egl_config, state->egl_context = eglCreateContext(state->egl_display, state->egl_config,
EGL_NO_CONTEXT, context_attributes); EGL_NO_CONTEXT, context_attributes);
@ -563,12 +543,6 @@ void chroma_egl_cleanup(chroma_state_t *state) {
return; return;
} }
// Clean up shared shader program while context is still current
if (state->shader_program != 0) {
glDeleteProgram(state->shader_program);
state->shader_program = 0;
}
if (state->egl_display != EGL_NO_DISPLAY) { if (state->egl_display != EGL_NO_DISPLAY) {
eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE, eglMakeCurrent(state->egl_display, EGL_NO_SURFACE, EGL_NO_SURFACE,
EGL_NO_CONTEXT); EGL_NO_CONTEXT);
@ -670,8 +644,8 @@ void chroma_surface_destroy(chroma_output_t *output) {
return; return;
} }
// Clean up OpenGL resources first. We use output's back-reference to state // Clean up OpenGL resources first
cleanup_gl_resources(output->state, output); cleanup_gl_resources(output);
if (output->egl_surface != EGL_NO_SURFACE) { if (output->egl_surface != EGL_NO_SURFACE) {
eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface); eglDestroySurface(eglGetCurrentDisplay(), output->egl_surface);
@ -693,8 +667,6 @@ void chroma_surface_destroy(chroma_output_t *output) {
output->surface = NULL; output->surface = NULL;
} }
output->configured = false;
// Log surface destruction // Log surface destruction
size_t surface_size = (size_t)output->width * (size_t)output->height * 4; size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
chroma_log_resource_deallocation("egl_surface", surface_size, chroma_log_resource_deallocation("egl_surface", surface_size,
@ -717,7 +689,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
if (init_gl_resources(state, output) != CHROMA_OK) { if (init_gl_resources(output) != CHROMA_OK) {
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
@ -790,7 +762,7 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
4 * sizeof(float), (void *)(2 * sizeof(float))); 4 * sizeof(float), (void *)(2 * sizeof(float)));
// Draw // Draw
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
// Unbind resources // Unbind resources
glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_ARRAY_BUFFER, 0);
@ -798,13 +770,16 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
glBindTexture(GL_TEXTURE_2D, 0); glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0); glUseProgram(0);
// Swap buffers; this implicitly commits the surface for EGL on Wayland // Swap buffers
if (!eglSwapBuffers(state->egl_display, output->egl_surface)) { if (!eglSwapBuffers(state->egl_display, output->egl_surface)) {
chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x", chroma_log("ERROR", "Failed to swap buffers for output %u: 0x%04x",
output->id, eglGetError()); output->id, eglGetError());
return CHROMA_ERROR_EGL; return CHROMA_ERROR_EGL;
} }
// Commit surface
wl_surface_commit(output->surface);
chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)", chroma_log("DEBUG", "Rendered wallpaper to output %u (cached resources)",
output->id); output->id);
return CHROMA_OK; return CHROMA_OK;

View file

@ -91,8 +91,9 @@ 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);
// Mark as configured - actual commit happens in render via eglSwapBuffers // Commit the surface to apply the acknowledgment
output->configured = true; wl_surface_commit(output->surface);
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);
@ -381,12 +382,6 @@ 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);