Compare commits

...

18 commits

Author SHA1 Message Date
29c298f71b
docs: match output matching format for sample config in README
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id71af8c0eced9312e0eca651e0f402b76a6a6964
2026-05-01 13:10:37 +03:00
9c813b2fa0
docs: fix config check order in sample config
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I213f87a6e4227a3813013a4f61f411566a6a6964
2026-05-01 13:10:36 +03:00
de6e8220e7
chroma: fix example config path in help text
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ibd4fd1267d18f72e45b5a4877ed366fa6a6a6964
2026-05-01 13:10:35 +03:00
f4275cb0f8
render: migrate rendering pipeline from OpenGL to GLES 2.0
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I55267367c8001ffc5ceac2c64015a7686a6a6964
2026-05-01 13:10:34 +03:00
f032d3723d
config: add cleanup and warnings for config parsing edge cases
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ifd08f33a0e6cc6e4f49966ea1c3f03f56a6a6964
2026-05-01 13:10:33 +03:00
edae535674
image: implement ref-counted image release
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idb30c621744eb9aa151fcaca012d93cc6a6a6964
2026-05-01 13:10:32 +03:00
26eda26620
meta: update CHROMA_VERSION header
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I302055656735d1ccb3ea025797cd9da86a6a6964
2026-05-01 13:10:31 +03:00
e1f534d0e6
nix: update package version; install sample TOML in postInstall
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Idd4a9425dd12beb3eb52faa1d046e55f6a6a6964
2026-05-01 13:10:30 +03:00
412ae1b933
docs: update README with new TOML-based config
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Iab7a0996b6733ae1b41c4a73fb2ab5256a6a6964
2026-05-01 13:10:29 +03:00
0c1e373dd9
nix: bump flake inputs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7c86de64449015ac8ed3be6cb1abae306a6a6964
2026-05-01 13:10:28 +03:00
c8ba7c4868
meta: replace the INI config sample with a TOML one
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia7864e1038a38c3fcdb0ba95947300bd6a6a6964
2026-05-01 13:10:27 +03:00
40227627c1
config: drop custom INI parser; migrate to TOML configurations
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6eabda96988b987d7397d6fc3cd47f2f6a6a6964
2026-05-01 13:10:26 +03:00
c84819b3e8
build: add tomlc17 dep
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7aa52879362f01cc2e61fe391f6ff4576a6a6964
2026-05-01 13:10:25 +03:00
8df203732e
meta: mark all vendored headers as linguist-vendored
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I7030f6ad2aeba80cdb08315011f731916a6a6964
2026-05-01 13:10:24 +03:00
c0b893887d
build: add a make target for generating sample config file
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9bbc0f7cc37599b4bebd07621c35db9f6a6a6964
2026-05-01 13:10:23 +03:00
5258a0b492
docs: mention protocol requirement
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I8c6fe27090a24df7a7ba5efce4bf1b786a6a6964
2026-05-01 13:10:22 +03:00
7306bbc625
build: tag 1.1.0
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I887f6c57dc16cd697061b995beab4a236a6a6964
2026-04-21 16:52:14 +03:00
raf
50c41fa883 Merge pull request 'config: add description-based output matching with desc: prefix' (#8) from notashelf/push-vlkvqnysylxt into main
Reviewed-on: #8
2026-04-21 13:49:20 +00:00
17 changed files with 3687 additions and 647 deletions

2
.gitattributes vendored
View file

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

View file

@ -1,5 +1,5 @@
PROJECT_NAME = chroma
VERSION = 1.0.1
VERSION = 2.0.0
# Directories
SRCDIR = src
@ -14,6 +14,9 @@ PREFIX ?= /usr/local
BINDIR_INSTALL = $(PREFIX)/bin
SYSTEMD_INSTALL = $(HOME)/.config/systemd/user
# Config file
CONFIG_FILE_NAME = chroma.toml
# Compiler and flags
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g
@ -46,8 +49,11 @@ LDFLAGS += -lm -ldl
# Source files (excluding generated protocol files)
SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))
VENDOR_SOURCES = $(INCDIR)/vendor/tomlc17.c
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)
DEPENDS = $(OBJECTS:.o=.d)
VENDOR_OBJECTS = $(VENDOR_SOURCES:$(INCDIR)/vendor/%.c=$(OBJDIR)/%.o)
ALL_OBJECTS = $(OBJECTS) $(VENDOR_OBJECTS)
DEPENDS = $(ALL_OBJECTS:.o=.d)
# Override object files for image.c and render.c to suppress third-party warnings
OBJECTS := $(filter-out $(OBJDIR)/image.o $(OBJDIR)/render.o,$(OBJECTS))
@ -78,9 +84,9 @@ $(INCDIR):
@mkdir -p $(INCDIR)
# Build main executable
$(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
$(TARGET): version-header $(PROTOCOL_HEADERS) $(ALL_OBJECTS) | $(BINDIR)
@echo " LINK $@"
@$(CC) $(OBJECTS) -o $@ $(LDFLAGS)
@$(CC) $(ALL_OBJECTS) -o $@ $(LDFLAGS)
# Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@ -95,6 +101,11 @@ $(OBJDIR)/render.o: $(SRCDIR)/render.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@echo " CC $<"
@$(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: CFLAGS = $(DEBUG_CFLAGS)
debug: $(TARGET)
@ -142,6 +153,11 @@ version-header:
# Create systemd service file
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:
@ -230,6 +246,7 @@ help:
@echo " check-deps - Check if all dependencies are available"
@echo " install - Install executable and systemd service"
@echo " uninstall - Remove installed files"
@echo " sample-config - Create sample configuration file"
@echo " clean - Remove build artifacts"
@echo " distclean - Remove all generated files"
@echo " format - Format source code (requires clang-format)"
@ -254,7 +271,7 @@ help:
-include $(DEPENDS)
# Phony targets
.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
.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
# Print variables
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,
PPM, PGM
- **EGL/OpenGL rendering**: Hardware-accelerated wallpaper rendering
- **Configuration file support**: Easy setup with INI-style config files
- **Simple Configuration file**: Easy setup with TOML config files
- **Signal handling**: Graceful shutdown and configuration reload (SIGHUP)
- **Intelligent downsampling**: Automatically reduces large image resolution to
save memory (up to 94% memory savings for 8K images)
@ -96,21 +96,40 @@ make sample-config
Chroma looks for configuration files in this order:
1. `~/.config/chroma/chroma.conf`
2. `$XDG_CONFIG_HOME/chroma/chroma.conf`
3. `./chroma.conf` (current directory)
1. `$XDG_CONFIG_HOME/chroma/chroma.toml`
2. `~/.config/chroma/chroma.toml`
3. `./chroma.toml` (current directory)
#### Sample Configuration
```ini
```toml
# Default wallpaper for outputs without specific mapping
default_image = ~/.config/chroma/default.jpg
default_image = "~/.config/chroma/default.jpg"
# Output-specific wallpapers
# Format: output.OUTPUT_NAME = /path/to/image.jpg
output.DP-1 = ~/Pictures/monitor1.jpg
output.DP-2 = ~/Pictures/monitor2.png
output.HDMI-A-1 = ~/Pictures/hdmi_wallpaper.jpg
# Format: [[output]] with name and image keys
[[output]]
name = "DP-1"
image = "~/Pictures/monitor1.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
@ -134,7 +153,7 @@ Options:
--version Show version information
Examples:
chroma -c ~/.config/chroma/chroma.conf
chroma -c ~/.config/chroma/chroma.toml
chroma --daemon
```
@ -183,10 +202,13 @@ Chroma works with any Wayland compositor that supports:
- `wl_compositor` interface
- `wl_output` interface
- `zwlr_layer_shell_v1` (wlr-layer-shell-unstable-v1) interface
- EGL window surface creation
Tested only on Hyprland, but should work fine with any compositor that meets the
above criteria. Which is basically all of them I think?
The wlr-layer-shell protocol is required for creating wallpaper surfaces on the
background layer. Tested only on Hyprland, but should work fine with any
wlroots-based compositor or any compositor that implements the wlr-layer-shell
protocol.
## Contributing

View file

@ -1,134 +0,0 @@
# 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

127
chroma.toml.sample Normal file
View file

@ -0,0 +1,127 @@
# 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": {
"nixpkgs": {
"locked": {
"lastModified": 1776074868,
"narHash": "sha256-XwIXdLWyLhr+7rCjKBc0i54ExC6/s0XEC4jT8ozI2S4=",
"lastModified": 1777312179,
"narHash": "sha256-AyQh4VtqwzVeLO1uHZ6/pzS6O96nXCXiUQZgCG+3X6k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fbb4cb95f81097fa79dae76545e2e7f6dc311f93",
"rev": "20123554ae9affff1a5bc969ad3ad02e839f49cf",
"type": "github"
},
"original": {

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

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

2915
include/vendor/tomlc17.c vendored Normal file

File diff suppressed because it is too large Load diff

190
include/vendor/tomlc17.h vendored Normal file
View file

@ -0,0 +1,190 @@
/* 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
stdenv.mkDerivation {
pname = "chroma";
version = "1.0.1";
version = "2.0.0";
src = fs.toSource {
root = s;
@ -64,7 +64,7 @@ in
'';
postInstall = ''
install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample
install -Dm755 ${../chroma.toml.sample} $out/share/chroma.toml.sample
'';
meta = {

View file

@ -5,29 +5,9 @@
#include <string.h>
#include <strings.h>
#include "../include/chroma.h"
#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
// Supports:
// - Exact name match: "DP-1" matches wl_output.name == "DP-1"
@ -60,31 +40,8 @@ static bool match_output(const char *pattern, const char *output_name,
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
static chroma_scale_mode_t parse_scale_mode(const char *value) {
chroma_scale_mode_t parse_scale_mode(const char *value) {
if (!value)
return CHROMA_SCALE_FILL; // default
@ -103,7 +60,7 @@ static chroma_scale_mode_t parse_scale_mode(const char *value) {
}
// Parse filter quality from string
static chroma_filter_quality_t parse_filter_quality(const char *value) {
chroma_filter_quality_t parse_filter_quality(const char *value) {
if (!value)
return CHROMA_FILTER_LINEAR; // default
@ -154,7 +111,7 @@ static const char *filter_quality_to_string(chroma_filter_quality_t quality) {
}
// Parse anchor position from string
static chroma_anchor_t parse_anchor(const char *value) {
chroma_anchor_t parse_anchor(const char *value) {
if (!value)
return CHROMA_ANCHOR_CENTER;
@ -213,12 +170,10 @@ static const char *anchor_to_string(chroma_anchor_t anchor) {
}
// Output-to-image mapping
static int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path,
chroma_scale_mode_t scale_mode,
chroma_filter_quality_t filter_quality,
chroma_anchor_t anchor, float anchor_x,
float anchor_y) {
int add_output_mapping(chroma_config_t *config, const char *output_name,
const char *image_path, chroma_scale_mode_t scale_mode,
chroma_filter_quality_t filter_quality,
chroma_anchor_t anchor, float anchor_x, float anchor_y) {
if (!config || !output_name || !image_path) {
return CHROMA_ERROR_INIT;
}
@ -268,6 +223,194 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
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
static void init_default_config(chroma_config_t *config) {
if (!config)
@ -291,313 +434,12 @@ static void init_default_config(chroma_config_t *config) {
config->max_output_height = 2160; // 4K height
config->min_scale_factor = 0.25f; // don't scale below 25%
// Set default image path (can be overridden)
const char *home = getenv("HOME");
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");
}
// Leave default_image empty - user must configure it explicitly
// This avoids errors when the hardcoded path doesn't exist
config->default_image[0] = '\0';
}
// 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
// Load configuration from file (TOML format only)
int chroma_config_load(chroma_config_t *config, const char *config_file) {
if (!config) {
return CHROMA_ERROR_INIT;
@ -611,52 +453,20 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) {
return CHROMA_OK;
}
FILE *file = fopen(config_file, "r");
if (!file) {
if (errno == ENOENT) {
chroma_log("INFO", "Config file not found: %s (using defaults)",
config_file);
return CHROMA_OK;
} else {
chroma_log("ERROR", "Failed to open config file %s: %s", config_file,
strerror(errno));
return CHROMA_ERROR_CONFIG;
}
// Check if file exists
if (!chroma_path_exists(config_file)) {
chroma_log("INFO", "Config file not found: %s (using defaults)",
config_file);
return CHROMA_OK;
}
chroma_log("INFO", "Loading configuration from: %s", config_file);
chroma_log("TRACE",
"Starting configuration parsing, estimated config size: %ld bytes",
chroma_get_file_size(config_file));
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++;
}
// Load TOML configuration
int result = chroma_config_load_toml(config, config_file);
if (result != CHROMA_OK) {
chroma_log("ERROR", "Failed to load TOML config from %s", config_file);
return result;
}
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
size_t config_size =
sizeof(chroma_config_t) +

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

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

View file

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

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