Compare commits
12 commits
d16a861e52
...
55012e16f9
| Author | SHA1 | Date | |
|---|---|---|---|
|
55012e16f9 |
|||
|
d402e6e300 |
|||
|
7ccb21af79 |
|||
|
5a1332080d |
|||
|
d145d88b7e |
|||
|
e5931e3910 |
|||
|
3d4974a128 |
|||
|
46ea940242 |
|||
|
ec628eb1af |
|||
|
5bfae35738 |
|||
|
746e83d3da |
|||
|
c9d32e14ab |
14 changed files with 2154 additions and 91 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
37
README.md
37
README.md
|
|
@ -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 -->
|
||||
|
|
|
|||
|
|
@ -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
6
flake.lock
generated
|
|
@ -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": {
|
||||
|
|
|
|||
16
flake.nix
16
flake.nix
|
|
@ -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
12
include/chroma.h
vendored
|
|
@ -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
1724
include/stb_image_write.h
Normal file
File diff suppressed because it is too large
Load diff
65
nix/package.nix
Normal file
65
nix/package.nix
Normal 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];
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
73
src/config.c
73
src/config.c
|
|
@ -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++) {
|
||||
|
|
|
|||
11
src/core.c
11
src/core.c
|
|
@ -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);
|
||||
|
|
|
|||
154
src/image.c
154
src/image.c
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
38
src/render.c
38
src/render.c
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue