Compare commits

...

12 commits

Author SHA1 Message Date
55012e16f9
docs: update README with 'new' features
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic45f39e98b73a0ec2e2ec8cbdcc2f5d66a6a6964
2026-01-31 15:15:16 +03:00
d402e6e300
nix: add packaging; update devshell
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I9f1ddf6dbd141b5a85b4b5a36c2c9a586a6a6964
2026-01-31 15:15:15 +03:00
7ccb21af79
nix: bump inputs
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I1889ab776432fbbbaa228a09c8acf4286a6a6964
2026-01-31 15:15:14 +03:00
5a1332080d
meta: ignore test files w/o wildcard
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I90cfb63920a7a42817f5f6c106a731b86a6a6964
2026-01-31 15:15:13 +03:00
d145d88b7e
meta: add downsampling options to sample config
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ie0aac45e64dfe292b3cd8de330f6b2d86a6a6964
2026-01-31 15:15:12 +03:00
e5931e3910
config: configure downsampling; remove config generator
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I43c5821edc0e121962bee76e39cb32816a6a6964
2026-01-31 15:15:11 +03:00
3d4974a128
core: optimize VBO
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic29424c13a4b2fbf6d74e6ec4c2bedde6a6a6964
2026-01-31 15:15:10 +03:00
46ea940242
image: initial downsampling implementation
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icec8c434ecf480c644a6f6e6a3b8cd5b6a6a6964
2026-01-31 15:15:09 +03:00
ec628eb1af
render: fix filtering; optimize VBO management
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I2f30f77e0f29437cac57a1064ca1f6796a6a6964
2026-01-31 15:15:08 +03:00
5bfae35738
meta: vendor stb_image_write header
We'll need this for funny image generation stuff

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I36c962a1b994d0a6717ac568421716816a6a6964
2026-01-31 15:15:07 +03:00
746e83d3da
meta: make makefile work again...
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Icaabb1719e4678d28439906c80ae54986a6a6964
2026-01-31 15:15:06 +03:00
c9d32e14ab
render: handle mipmaps properly
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I43de088ca17559648d67e728db1179cf6a6a6964
2026-01-31 15:14:55 +03:00
14 changed files with 2154 additions and 91 deletions

3
.gitignore vendored
View file

@ -8,6 +8,7 @@ src/xdg-shell.c
src/wlr-layer-shell-unstable-v1.c
# Ignore test stuff that I create to... test stuff.
test_*
test_memory
test_config
*.jpg
*.conf

View file

@ -116,7 +116,9 @@ version-header:
@echo "#ifndef CHROMA_VERSION_H" > $(INCDIR)/chroma_version.h
@echo "#define CHROMA_VERSION_H" >> $(INCDIR)/chroma_version.h
@echo "" >> $(INCDIR)/chroma_version.h
@echo "#ifndef CHROMA_VERSION" >> $(INCDIR)/chroma_version.h
@echo "#define CHROMA_VERSION \"$(VERSION)\"" >> $(INCDIR)/chroma_version.h
@echo "#endif" >> $(INCDIR)/chroma_version.h
@echo "" >> $(INCDIR)/chroma_version.h
@echo "#endif // CHROMA_VERSION_H" >> $(INCDIR)/chroma_version.h

View file

@ -1,24 +1,39 @@
# Chroma
A simple, lightweight and efficientt wallpaper daeemon for Wayland compositors
with multi-monitor & automatic hotplugging support. Born from my woes with
Hyprpaper, swaybg and most ironically SWWW, which turned out to be NOT a
solution to my Wayland wallpaper woes.
Super-fast, lightweight and efficient wallpaper daemon for Wayland compositors
with multi-monitor & hotplugging support. Born from my woes with Hyprpaper,
swaybg and most ironically _SWWW_ (now called AWWW for some awful reason), which
turned out to be NOT a solution to my Wayland wallpaper woes. Smaller, faster,
cleaner and somehow still more feature-rich...
## Features
I did not expect to be writing something too feature-rich, but I still ended up
with something relatively convoluted. Chroma (mostly) reliably supports:
Chroma combines simplicity with powerful performance optimizations and
comprehensive monitor management. Here's what makes Chroma stand out:
- **Multi-monitors**: Automatically detects and manages wallpapers for all
connected displays
### Core Functionality
- **Memory-efficient**: Smart caching and resource management
- **Optimized rendering**: VBO dirty flagging and reduced GPU overhead
- **Multi-monitor support**: Automatically detects and manages wallpapers for
all connected displays
- **Configurable scaling modes**: Fill, fit, stretch, and center options per
monitor
- **Hotplugging**: Dynamically handles monitor connection/disconnection events
- **Per-output configuration**: Set different wallpapers for each monitor
- **Per-output configuration**: Set different wallpapers and settings for each
monitor
- **Global and per-output settings**: Override defaults for specific monitors
- **Performance tuning**: Adjustable downsampling limits and quality
thresholds
- **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
- **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)
- **Advanced filtering**: Nearest, linear, bilinear, and trilinear filtering
options
## Usage
@ -198,7 +213,7 @@ make analyze # requires cppcheck
- C11 standard
- 2-space indentation
- No tabs
- No tabs (except for the Makefile, obviously)
- Function names: `chroma_function_name`
- Constants: `CHROMA_CONSTANT_NAME`
@ -209,3 +224,5 @@ make analyze # requires cppcheck
This project is made available under Mozilla Public License (MPL) version 2.0.
See [LICENSE](LICENSE) for more details on the exact conditions. An online copy
is provided [here](https://www.mozilla.org/en-US/MPL/2.0/).
<!--markdownlint-enable MD059 -->

View file

@ -27,15 +27,59 @@ default_image = ~/.config/chroma/default.jpg
# 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
# 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
# ==================================
# Format: output.OUTPUT_NAME = /path/to/image.ext
# Basic format: output.OUTPUT_NAME = /path/to/image.ext
#
# 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
#
# To find your output names, run one of these commands:
# - wlr-randr (for wlroots-based compositors like Sway, Hyprland)
#
# Compositor Agnostic:
# - wlr-randr (for wlroots-based compositors)
# - wayland-info | grep wl_output
# - kanshi list-outputs
#
# Compositor Specific
# - hyprctl monitors -j | jq .[].name (Hyprland specific)
#
# Common output name patterns:
# - DP-1, DP-2, DP-3, etc. (DisplayPort)
# - HDMI-A-1, HDMI-A-2, etc. (HDMI)
@ -43,6 +87,14 @@ daemon_mode = false
# - DVI-D-1, DVI-I-1 (DVI)
# - VGA-1 (VGA, legacy)
#
# Example:
# 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-2 = ~/Pictures/monitor2.jpg
# output.DP-2.scale = stretch
# output.eDP-1 = ~/Pictures/laptop-wallpaper.jpg
# output.eDP-1.scale = center
# output.eDP-1.filter = trilinear

6
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1759155593,
"narHash": "sha256-potIJyEY7ExgfiMzr44/KBODFednyzRUAE2vs4aThHs=",
"lastModified": 1768665130,
"narHash": "sha256-N9eZwqdrubgKRDICaJ92Q5UMghBR1nbHnokSgZ21EJI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "13ca7febc86978bdb67c0ae94f568b189ae84eef",
"rev": "c444f4a39d8efbf5e07d678ffde3e661735e1d7e",
"type": "github"
},
"original": {

View file

@ -1,8 +1,11 @@
{
description = "Wayland Wallpaper Daemon";
description = "Super-fast, lightweight and efficient wallpaper daemon for Wayland compositors";
inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref?nixos-unstable";
outputs = {nixpkgs, ...}: let
outputs = {
nixpkgs,
self,
...
}: let
systems = ["x86_64-linux" "aarch64-linux"];
forAllSystems = f:
builtins.listToAttrs (map (system: {
@ -18,5 +21,12 @@
in {
default = pkgs.callPackage ./shell.nix {};
});
packages = forAllSystems (system: let
pkgs = pkgsFor system;
in {
chroma = pkgs.callPackage ./nix/package.nix {};
default = self.packages.${system}.chroma;
});
};
}

12
include/chroma.h vendored
View file

@ -99,6 +99,7 @@ typedef struct {
GLuint ebo;
bool gl_resources_initialized;
bool texture_uploaded;
bool vbo_dirty; // track VBO needs update
} chroma_output_t;
// Config mapping structure
@ -119,6 +120,12 @@ typedef struct {
// Global scaling and filtering settings (used as defaults)
chroma_scale_mode_t default_scale_mode;
chroma_filter_quality_t default_filter_quality;
// Image downsampling settings
bool enable_downsampling; // enable automatic downsampling
int max_output_width; // maximum expected output width
int max_output_height; // maximum expected output height
float min_scale_factor; // minimum scale factor (don't scale below this)
} chroma_config_t;
// Main application state
@ -205,7 +212,8 @@ 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);
int chroma_image_load(chroma_image_t *image, const char *path,
const chroma_config_t *config);
void chroma_image_free(chroma_image_t *image);
chroma_image_t *chroma_image_find_by_path(chroma_state_t *state,
const char *path);
@ -224,7 +232,7 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
int chroma_config_get_mapping_for_output(
chroma_config_t *config, const char *output_name,
chroma_scale_mode_t *scale_mode, chroma_filter_quality_t *filter_quality);
int chroma_config_create_sample(const char *config_file);
void chroma_config_print(const chroma_config_t *config);
// Main loop and events

1724
include/stb_image_write.h Normal file

File diff suppressed because it is too large Load diff

65
nix/package.nix Normal file
View file

@ -0,0 +1,65 @@
{
lib,
stdenv,
wayland,
wayland-protocols,
wayland-scanner,
libxkbcommon,
libGL,
mesa,
glibc,
pkg-config,
}: let
fs = lib.fileset;
s = ../.;
in
stdenv.mkDerivation {
pname = "chroma";
version = "1.0.0";
src = fs.toSource {
root = s;
fileset = fs.unions [
(s + /include)
(s + /protocols)
(s + /src)
(s + /Makefile)
];
};
buildInputs = [
# Wayland libraries
wayland.dev
wayland-protocols
wayland-scanner
libxkbcommon
# EGL/OpenGL libraries
libGL
mesa
# System libraries
glibc.dev
];
nativeBuildInputs = [
pkg-config
];
makeFlags = [
"PREFIX=$(out)"
"SYSTEMD_DIR=$(out)/lib/systemd/system"
];
postInstall = ''
install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample
'';
meta = {
description = "Super-fast, lightweight and efficient wallpaper daemon for Wayland compositors";
license = lib.licenses.mpl20;
mainProgram = "chroma";
maintainers = with lib.maitainers; [NotAShelf];
};
}

View file

@ -25,6 +25,9 @@ pkgs.mkShell {
# System libraries
glibc.dev
# For Tests
(python313.withPackages (ps: with ps; [matplotlib numpy]))
];
nativeBuildInputs = with pkgs; [
@ -44,8 +47,5 @@ pkgs.mkShell {
'';
# Environment variables for the build system
env = {
CHROMA_VERSION = "1.0.0";
WAYLAND_DEBUG = 1;
};
env.WAYLAND_DEBUG = 1;
}

View file

@ -184,6 +184,12 @@ static void init_default_config(chroma_config_t *config) {
config->default_scale_mode = CHROMA_SCALE_FILL;
config->default_filter_quality = CHROMA_FILTER_LINEAR;
// Set default downsampling settings
config->enable_downsampling = true; // enable by default, performance etc.
config->max_output_width = 3840; // 4K width
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) {
@ -276,6 +282,34 @@ static int parse_config_line(chroma_config_t *config, char *line,
"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, "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 = atof(value);
if (factor > 0.0f && factor <= 1.0f) { // Valid range
config->min_scale_factor = factor;
chroma_log("DEBUG", "Set minimum scale factor: %.2f", 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;
@ -515,39 +549,7 @@ int chroma_config_get_mapping_for_output(
return CHROMA_OK;
}
// Create a sample configuration file
int chroma_config_create_sample(const char *config_file) {
if (!config_file) {
return CHROMA_ERROR_INIT;
}
FILE *file = fopen(config_file, "w");
if (!file) {
chroma_log("ERROR", "Failed to create sample config file %s: %s",
config_file, strerror(errno));
return CHROMA_ERROR_CONFIG;
}
fprintf(file, "# Chroma Wallpaper Daemon Configuration\n");
fprintf(file, "# Lines starting with # are comments\n\n");
fprintf(file, "# Default wallpaper for outputs without specific mapping\n");
fprintf(file, "default_image = ~/.config/chroma/default.jpg\n\n");
fprintf(file, "# Output-specific wallpapers\n");
fprintf(file, "# Format: output.OUTPUT_NAME = /path/to/image.jpg\n");
fprintf(file, "# You can find output names using: wlr-randr\n");
fprintf(file, "\n");
fprintf(file, "# Examples:\n");
fprintf(file, "# output.DP-1 = ~/.config/chroma/monitor1.jpg\n");
fprintf(file, "# output.DP-2 = ~/.config/chroma/monitor2.png\n");
fprintf(file, "# output.HDMI-A-1 = ~/.config/chroma/hdmi.jpg\n");
fclose(file);
chroma_log("INFO", "Created sample configuration file: %s", config_file);
return CHROMA_OK;
}
// Print current configuration for debugging
void chroma_config_print(const chroma_config_t *config) {
@ -562,6 +564,13 @@ void chroma_config_print(const chroma_config_t *config) {
scale_mode_to_string(config->default_scale_mode));
chroma_log("INFO", "Default filter quality: %s",
filter_quality_to_string(config->default_filter_quality));
chroma_log("INFO", "Downsampling: %s",
config->enable_downsampling ? "enabled" : "disabled");
if (config->enable_downsampling) {
chroma_log("INFO", "Max output size: %dx%d", config->max_output_width,
config->max_output_height);
chroma_log("INFO", "Min scale factor: %.2f", config->min_scale_factor);
}
chroma_log("INFO", "Output mappings: %d", config->mapping_count);
for (int i = 0; i < config->mapping_count; i++) {

View file

@ -96,6 +96,7 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
bool image_changed = (output->image != image);
if (image_changed && 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",
output->id);
}
@ -103,6 +104,11 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
// Assign image to output
output->image = image;
// Mark VBO as dirty if image changed
if (image_changed) {
output->vbo_dirty = true;
}
// Store old configuration values for comparison
chroma_scale_mode_t old_scale_mode = output->scale_mode;
chroma_filter_quality_t old_filter_quality = output->filter_quality;
@ -120,6 +126,7 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
if (had_config && (old_scale_mode != output->scale_mode ||
old_filter_quality != output->filter_quality)) {
chroma_output_invalidate_texture(output);
output->vbo_dirty = true; // VBO needs update for new scale mode
chroma_log("DEBUG",
"Configuration changed for output %u, invalidated texture",
output->id);
@ -273,14 +280,14 @@ int chroma_run(chroma_state_t *state) {
break;
}
// Use select() to wait for events with timeout
// Use `select()` to wait for events with longer timeout to reduce CPU usage
fd_set readfds;
struct timeval timeout;
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
timeout.tv_sec = 1; // 1s timeout
timeout.tv_sec = 10;
timeout.tv_usec = 0;
int select_result = select(fd + 1, &readfds, NULL, NULL, &timeout);

View file

@ -24,8 +24,76 @@ static long get_file_size(const char *path) {
return st.st_size;
}
// Load image from file
int chroma_image_load(chroma_image_t *image, const char *path) {
// Calculate optimal image size based on output dimensions
static void calculate_optimal_size(int original_width, int original_height,
int max_output_width, int max_output_height,
int *optimal_width, int *optimal_height) {
// If image is smaller than outputs, keep original size
if (original_width <= max_output_width &&
original_height <= max_output_height) {
*optimal_width = original_width;
*optimal_height = original_height;
return;
}
// Calculate scale factor to fit within max output dimensions
float scale_x = (float)max_output_width / original_width;
float scale_y = (float)max_output_height / original_height;
float scale = (scale_x < scale_y) ? scale_x : scale_y;
// Apply scale factor with minimum size to avoid too small images
scale = (scale > 1.0f) ? 1.0f : scale;
scale = (scale < 0.25f) ? 0.25f : scale; // XXX: don't scale below 25%
*optimal_width = (int)(original_width * scale);
*optimal_height = (int)(original_height * scale);
// Ensure even dimensions for better GPU alignment
*optimal_width = (*optimal_width / 2) * 2;
*optimal_height = (*optimal_height / 2) * 2;
}
// FIXME: this is a very simple way of implementing box filter downsampling for
// memory efficiency Could be better, but this is good enough *for the time
// 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) {
if (!src_data || !dst_data) {
return -1;
}
float x_ratio = (float)src_width / dst_width;
float y_ratio = (float)src_height / dst_height;
for (int y = 0; y < dst_height; y++) {
for (int x = 0; x < dst_width; x++) {
// Calculate corresponding source pixel
int src_x = (int)(x * x_ratio);
int src_y = (int)(y * y_ratio);
// Ensure we're within bounds
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;
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
}
}
return 0;
}
// Load image from file with configurable downsampling
int chroma_image_load(chroma_image_t *image, const char *path,
const chroma_config_t *config) {
if (!image || !path) {
chroma_log("ERROR", "Invalid parameters for image loading");
return CHROMA_ERROR_INIT;
@ -77,15 +145,87 @@ int chroma_image_load(chroma_image_t *image, const char *path) {
return CHROMA_ERROR_IMAGE;
}
// Store original dimensions before potential downsampling
int original_width = image->width;
int original_height = image->height;
// Apply intelligent downsampling if enabled
bool should_downsample = false;
int optimal_width = original_width;
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);
// Apply minimum scale factor constraint
float scale_x = (float)optimal_width / original_width;
float scale_y = (float)optimal_height / original_height;
float scale = (scale_x < scale_y) ? scale_x : scale_y;
if (scale < config->min_scale_factor) {
scale = config->min_scale_factor;
optimal_width = (int)(original_width * scale);
optimal_height = (int)(original_height * scale);
// Ensure even dimensions
optimal_width = (optimal_width / 2) * 2;
optimal_height = (optimal_height / 2) * 2;
}
should_downsample =
(optimal_width < original_width || optimal_height < original_height);
}
// Downsamp if needed and enabled
if (should_downsample) {
chroma_log("INFO",
"Downsampling image from %dx%d to %dx%d (%.1f%% of original)",
original_width, original_height, optimal_width, optimal_height,
(float)(optimal_width * optimal_height) /
(original_width * original_height) * 100.0f);
size_t optimal_size = (size_t)optimal_width * optimal_height * 4;
unsigned char *downsampled_data = malloc(optimal_size);
if (!downsampled_data) {
chroma_log("ERROR", "Failed to allocate memory for downsampled image");
chroma_image_free(image);
return CHROMA_ERROR_MEMORY;
}
if (downsample_image(image->data, original_width, original_height,
downsampled_data, optimal_width,
optimal_height) != 0) {
chroma_log("ERROR", "Failed to downsample image");
free(downsampled_data);
chroma_image_free(image);
return CHROMA_ERROR_IMAGE;
}
stbi_image_free(image->data);
image->data = downsampled_data;
image->width = optimal_width;
image->height = optimal_height;
chroma_log("DEBUG", "Successfully downsampled image to %dx%d",
optimal_width, optimal_height);
} else if (config && !config->enable_downsampling) {
chroma_log("DEBUG",
"Downsampling disabled, keeping original resolution %dx%d",
original_width, original_height);
}
image->loaded = true;
// Calculate and log memory allocation
size_t image_size = (size_t)image->width * image->height * image->channels;
chroma_log_resource_allocation("image_data", image_size, path);
chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)", path,
chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)%s", path,
image->width, image->height, image->channels,
(double)image_size / (1024.0 * 1024.0));
(double)image_size / (1024.0 * 1024.0),
should_downsample ? " (downsampled)" : "");
return CHROMA_OK;
}
@ -159,8 +299,8 @@ chroma_image_t *chroma_image_get_or_load(chroma_state_t *state,
state->image_count++;
}
// Load the image
if (chroma_image_load(image, path) != CHROMA_OK) {
// Load the image with configuration
if (chroma_image_load(image, path, &state->config) != CHROMA_OK) {
// If this was a new slot, decrement count
if (!existing) {
state->image_count--;
@ -241,7 +381,7 @@ void chroma_image_init_stb(void) {
// Set stb_image options
stbi_set_flip_vertically_on_load(0);
// These could be made configurable
// FIXME: these could be made configurable
stbi_ldr_to_hdr_gamma(2.2f);
stbi_ldr_to_hdr_scale(1.0f);

View file

@ -10,27 +10,33 @@
// Convert filter quality enum to OpenGL parameters
static void get_gl_filter_params(chroma_filter_quality_t quality,
GLint *min_filter, GLint *mag_filter) {
GLint *min_filter, GLint *mag_filter,
bool *needs_mipmaps) {
switch (quality) {
case CHROMA_FILTER_NEAREST:
*min_filter = GL_NEAREST;
*mag_filter = GL_NEAREST;
*needs_mipmaps = false;
break;
case CHROMA_FILTER_LINEAR:
*min_filter = GL_LINEAR;
*mag_filter = GL_LINEAR;
*needs_mipmaps = false;
break;
case CHROMA_FILTER_BILINEAR:
*min_filter = GL_LINEAR_MIPMAP_LINEAR;
*min_filter = GL_LINEAR;
*mag_filter = GL_LINEAR;
*needs_mipmaps = false;
break;
case CHROMA_FILTER_TRILINEAR:
*min_filter = GL_LINEAR_MIPMAP_LINEAR;
*mag_filter = GL_LINEAR;
*needs_mipmaps = true;
break;
default:
*min_filter = GL_LINEAR;
*mag_filter = GL_LINEAR;
*needs_mipmaps = false;
break;
}
}
@ -272,6 +278,7 @@ static int init_gl_resources(chroma_output_t *output) {
GL_STATIC_DRAW);
output->texture_id = 0; // will be created when image is assigned
output->vbo_dirty = true; // VBO needs initial update
output->gl_resources_initialized = true;
chroma_log("DEBUG", "Initialized GL resources for output %u", output->id);
@ -324,7 +331,9 @@ static int update_texture_from_image(chroma_output_t *output,
// Use configured filter quality
GLint min_filter, mag_filter;
get_gl_filter_params(filter_quality, &min_filter, &mag_filter);
bool needs_mipmaps;
get_gl_filter_params(filter_quality, &min_filter, &mag_filter,
&needs_mipmaps);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, mag_filter);
@ -332,6 +341,12 @@ static int update_texture_from_image(chroma_output_t *output,
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->width, image->height, 0,
GL_RGBA, GL_UNSIGNED_BYTE, image->data);
// Generate mipmaps for trilinear filtering if they're needed
if (needs_mipmaps) {
glGenerateMipmap(GL_TEXTURE_2D);
chroma_log("DEBUG", "Generated mipmaps for trilinear filtering");
}
glBindTexture(GL_TEXTURE_2D, 0);
// Mark this output as having uploaded its texture
@ -670,11 +685,14 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
glBindTexture(GL_TEXTURE_2D, output->texture_id);
glUniform1i(glGetUniformLocation(output->shader_program, "texture"), 0);
// Update VBO only if needed. E.g, image changed, scale mode changed, or first
// render
if (output->vbo_dirty) {
// Calculate texture coordinates based on scaling mode
float tex_coords[8];
calculate_texture_coords(output->scale_mode, output->image->width,
output->image->height, output->width, output->height,
tex_coords);
output->image->height, output->width,
output->height, tex_coords);
// Create dynamic vertex data with calculated texture coordinates
float dynamic_vertices[] = {
@ -691,6 +709,13 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
dynamic_vertices);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
output->vbo_dirty = false; // mark VBO as up to date
} else {
// Just bind the existing buffers
glBindBuffer(GL_ARRAY_BUFFER, output->vbo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, output->ebo);
}
// Set vertex attributes
GLint position_attr = glGetAttribLocation(output->shader_program, "position");
GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
@ -739,4 +764,7 @@ void chroma_output_invalidate_texture(chroma_output_t *output) {
output->texture_uploaded = false; // reset upload flag
chroma_log("DEBUG", "Invalidated texture cache for output %u", output->id);
}
// Mark VBO as dirty since texture coordinates may need recalculation
output->vbo_dirty = true;
}