Merge pull request 'config: add description-based output matching with desc: prefix' (#8) from notashelf/push-vlkvqnysylxt into main

Reviewed-on: #8
This commit is contained in:
raf 2026-04-21 13:49:20 +00:00
commit 50c41fa883
20 changed files with 1522 additions and 146 deletions

2
.gitignore vendored
View file

@ -12,3 +12,5 @@ test_memory
test_config
*.jpg
*.conf
vgcore.*
*_report.txt

View file

@ -17,8 +17,13 @@ SYSTEMD_INSTALL = $(HOME)/.config/systemd/user
# Compiler and flags
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g
CFLAGS += -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)\"
CPPFLAGS = -I$(INCDIR)
CFLAGS += -fstack-protector-strong -fstack-clash-protection
CFLAGS += -fno-common -Wconversion -Wshadow -Wstrict-prototypes
CFLAGS += -Wdouble-promotion -Wformat=2 -Wnormalized=nfc
CFLAGS += -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)\"
# Include path for generated headers
CPPFLAGS = -I$(INCDIR) -I$(INCDIR)/vendor -isystem $(INCDIR)/vendor
# Debug build flags
DEBUG_CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -Og -g3 -DDEBUG
@ -44,6 +49,10 @@ SOURCES = $(filter-out $(PROTOCOL_SOURCES), $(wildcard $(SRCDIR)/*.c))
OBJECTS = $(SOURCES:$(SRCDIR)/%.c=$(OBJDIR)/%.o) $(PROTOCOL_OBJECTS)
DEPENDS = $(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))
OBJECTS += $(OBJDIR)/image.o $(OBJDIR)/render.o
# Default target
TARGET = $(BINDIR)/$(PROJECT_NAME)
all: $(TARGET)
@ -76,7 +85,15 @@ $(TARGET): version-header $(PROTOCOL_HEADERS) $(OBJECTS) | $(BINDIR)
# Compile source files
$(OBJDIR)/%.o: $(SRCDIR)/%.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@echo " CC $<"
@$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -c $< -o $@
@$(CC) $(CPPFLAGS) $(CFLAGS) -MMD -MP -Wno-error -c $< -o $@
$(OBJDIR)/image.o: $(SRCDIR)/image.c $(PROTOCOL_HEADERS) | $(OBJDIR)
@echo " CC $<"
@$(CC) $(CPPFLAGS) $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion -MMD -MP -Wno-error -c $< -o $@
$(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 $@
# Debug build
debug: CFLAGS = $(DEBUG_CFLAGS)
@ -131,11 +148,12 @@ clean:
@echo "Cleaning build artifacts..."
rm -rf $(OBJDIR) $(BINDIR)
rm -f $(PROTOCOL_HEADERS) $(PROTOCOL_SOURCES)
rm -f "vcore.*"
# Format source code (requires clang-format)
format:
@echo "Formatting source code..."
@find $(SRCDIR) $(INCDIR) -name "*.c" -o -name "*.h" | xargs clang-format -i
@find $(SRCDIR) -name "*.c" -o -name "*.h" | grep -v '/vendor/' | xargs clang-format -i
# Static analysis (requires cppcheck)
analyze:
@ -147,10 +165,33 @@ analyze:
$(SRCDIR)
# Run tests
# FIXME: add tests
test: $(TARGET)
@echo "Running tests..."
@echo "Tests not implemented yet."
@echo "Running unit tests..."
@$(CC) -o bin/test tests/test.c lib/test_common.c \
-I./include -I./include/vendor -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion
@./bin/test
# Run benchmarks
bench:
@echo "Running performance benchmarks..."
@$(CC) -o bin/bench benchmarks/bench.c lib/test_common.c \
-I./include -I./include/vendor -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion
@./bin/bench
# Memory analysis tests
test-memory:
@echo "Building memory tests..."
@$(CC) -o bin/test tests/test.c lib/test_common.c \
-I./include -I./include/vendor -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion
@valgrind --leak-check=full --show-leak-kinds=all ./bin/test 2>&1 | tee tests/memory_report.txt
@echo "Analysis complete. See tests/memory_report.txt"
# Generate memory profile CSVs
profile-memory:
@$(CC) -o bin/test tests/test.c lib/test_common.c \
-I./include -I./include/vendor -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) -Wno-sign-conversion -Wno-double-promotion -Wno-conversion
@./bin/test --profile
@echo "CSV files generated in /tmp/"
# Version management targets
bump-patch:
@ -213,7 +254,7 @@ help:
-include $(DEPENDS)
# Phony targets
.PHONY: all debug static check-deps install uninstall systemd-service version-header sample-config clean distclean format analyze test help bump-patch bump-minor bump-major set-version
.PHONY: all debug static check-deps install uninstall systemd-service version-header clean distclean format analyze test test-memory memory-report help bump-patch bump-minor bump-major set-version
# Print variables
print-%:

View file

@ -53,6 +53,9 @@ comprehensive monitor management. Here's what makes Chroma stand out:
- Wayland development headers
- EGL/OpenGL development headers
See [development section](#development) for more details. This section might get
outdated at any given moment, so refer to the Nix shell if in doubt.
### Building
#### Quick Build
@ -66,6 +69,9 @@ make
# Or build debug version
make debug
# Alternatively, create a static build
make static
```
### Installation
@ -179,44 +185,56 @@ Chroma works with any Wayland compositor that supports:
- `wl_output` interface
- EGL window surface creation
Tested only on Hyprland.
Tested only on Hyprland, but should work fine with any compositor that meets the
above criteria. Which is basically all of them I think?
## Development
## Contributing
### Building Debug Version
You might want to contribute to Chroma for a variety of reasons. I usually will
not judge, however, there are some conventions I expect you to adhere to. Mainly
I would like for you to follow the project's **code style**:
- C11 standard (I really wished for C99)
- 2-space indentation (use `make format`)
- No tabs (except for the Makefile, obviously)
- Function names: `chroma_function_name`
- Constants: `CHROMA_CONSTANT_NAME`
Once your changes are done, fork this repository and create a feature branch.
This is not a strict requirement but I'd rather not deal with rebase failures.
Create your feature branch, make your changes, _test thoroughly_ and submit your
pull request when you are done. With your pull request, I'd _really_ like a tiny
snippet of text that explains your motive of changes. While I can infer what you
are trying to do, I'd rather _know_ what was going on in your head.
### Development
A Nix shell is provided within the repository. You may use both `nix-shell` and
`nix develop` to enter a development shell with all of the required dependencies
for _dynamic linking_. Additionally, [Direnv](https://direnv.net) users may use
`direnv allow` to use the shell provided by the repository.
A few convenience commands are provided by the Makefile, which you may invoke at
your own discretion.
#### Building Debug Version
```bash
make debug
```
### Code Formatting
#### Code Formatting
```bash
make format # requires clang-format
```
### Static Analysis
#### Static Analysis
```bash
make analyze # requires cppcheck
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
### Code Style
- C11 standard
- 2-space indentation
- No tabs (except for the Makefile, obviously)
- Function names: `chroma_function_name`
- Constants: `CHROMA_CONSTANT_NAME`
## License
<!--markdownlint-disable MD059 -->

332
benchmarks/bench.c Normal file
View file

@ -0,0 +1,332 @@
#include "test_common.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/time.h>
typedef struct {
double time_ms;
double pixels_per_sec;
double megabytes_per_sec;
size_t input_bytes;
size_t output_bytes;
} BenchResult;
static double get_time_us(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000000.0 + tv.tv_usec;
}
static void calculate_bench_metrics(const char *name, int iterations, BenchResult *r) {
if (strstr(name, "create_uniform") != NULL) {
r->input_bytes = 0;
r->output_bytes = 16 * 16 * 4;
} else if (strstr(name, "create_gradient_256") != NULL) {
r->input_bytes = 0;
r->output_bytes = 256 * 256 * 4;
} else if (strstr(name, "create_noise_1024") != NULL) {
r->input_bytes = 0;
r->output_bytes = 1024 * 1024 * 4;
} else if (strstr(name, "downsample_uniform_16x16") != NULL) {
r->input_bytes = 16 * 16 * 4;
r->output_bytes = 8 * 8 * 4;
} else if (strstr(name, "downsample_gradient_64x64") != NULL) {
r->input_bytes = 64 * 64 * 4;
r->output_bytes = 32 * 32 * 4;
} else if (strstr(name, "downsample_gradient_256x256") != NULL) {
r->input_bytes = 256 * 256 * 4;
r->output_bytes = 128 * 128 * 4;
} else if (strstr(name, "downsample_gradient_1024x1024") != NULL) {
r->input_bytes = 1024 * 1024 * 4;
r->output_bytes = 512 * 512 * 4;
} else if (strstr(name, "downsample_noise_512x512") != NULL) {
r->input_bytes = 512 * 512 * 4;
r->output_bytes = 128 * 128 * 4;
} else if (strstr(name, "downsample_noise_1024x1024") != NULL) {
r->input_bytes = 1024 * 1024 * 4;
r->output_bytes = 256 * 256 * 4;
} else if (strstr(name, "downsample_noise_1920x1080") != NULL) {
r->input_bytes = 1920 * 1080 * 4;
r->output_bytes = 960 * 540 * 4;
} else if (strstr(name, "downsample_noise_3840x2160") != NULL) {
r->input_bytes = 3840 * 2160 * 4;
r->output_bytes = 1920 * 1080 * 4;
} else if (strstr(name, "downsample_noise_4096x4096") != NULL) {
r->input_bytes = 4096 * 4096 * 4;
r->output_bytes = 1024 * 1024 * 4;
} else if (strstr(name, "downsample_checkerboard_100x100") != NULL) {
r->input_bytes = 100 * 100 * 4;
r->output_bytes = 50 * 50 * 4;
} else if (strstr(name, "downsample_checkerboard_256x256") != NULL) {
r->input_bytes = 256 * 256 * 4;
r->output_bytes = 128 * 128 * 4;
} else {
r->input_bytes = 0;
r->output_bytes = 0;
}
r->input_bytes *= (size_t)iterations;
r->output_bytes *= (size_t)iterations;
}
static void run_bench(double (*fn)(void), int iterations, double *elapsed_ms) {
double start = get_time_us();
for (int i = 0; i < iterations; i++) {
fn();
}
*elapsed_ms = (get_time_us() - start) / 1000.0;
}
static double bench_create_uniform_16x16(void) {
for (int i = 0; i < 5000; i++) {
uint8_t *img = create_uniform_image(16, 16, 128, 128, 128);
free(img);
}
return 0;
}
static double bench_create_gradient_256x256(void) {
for (int i = 0; i < 500; i++) {
uint8_t *img = create_gradient_image(256, 256);
free(img);
}
return 0;
}
static double bench_create_noise_1024x1024(void) {
for (int i = 0; i < 50; i++) {
uint8_t *img = create_noise_image(1024, 1024, 42);
free(img);
}
return 0;
}
static double bench_downsample_uniform_16x16(void) {
uint8_t *src = create_uniform_image(16, 16, 128, 128, 128);
int dw, dh;
for (int i = 0; i < 2000; i++) {
uint8_t *dst = downsample_image(src, 16, 16, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_gradient_64x64(void) {
uint8_t *src = create_gradient_image(64, 64);
int dw, dh;
for (int i = 0; i < 500; i++) {
uint8_t *dst = downsample_image(src, 64, 64, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_gradient_256x256(void) {
uint8_t *src = create_gradient_image(256, 256);
int dw, dh;
for (int i = 0; i < 200; i++) {
uint8_t *dst = downsample_image(src, 256, 256, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_gradient_1024x1024(void) {
uint8_t *src = create_gradient_image(1024, 1024);
int dw, dh;
for (int i = 0; i < 20; i++) {
uint8_t *dst = downsample_image(src, 1024, 1024, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_noise_512x512(void) {
uint8_t *src = create_noise_image(512, 512, 42);
int dw, dh;
for (int i = 0; i < 50; i++) {
uint8_t *dst = downsample_image(src, 512, 512, 4, &dw, &dh, 0.25f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_noise_1024x1024(void) {
uint8_t *src = create_noise_image(1024, 1024, 123);
int dw, dh;
for (int i = 0; i < 15; i++) {
uint8_t *dst = downsample_image(src, 1024, 1024, 4, &dw, &dh, 0.25f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_noise_1920x1080(void) {
uint8_t *src = create_noise_image(1920, 1080, 456);
int dw, dh;
for (int i = 0; i < 5; i++) {
uint8_t *dst = downsample_image(src, 1920, 1080, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_noise_3840x2160(void) {
uint8_t *src = create_noise_image(3840, 2160, 789);
int dw, dh;
for (int i = 0; i < 2; i++) {
uint8_t *dst = downsample_image(src, 3840, 2160, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_noise_4096x4096(void) {
uint8_t *src = create_noise_image(4096, 4096, 456);
int dw, dh;
for (int i = 0; i < 2; i++) {
uint8_t *dst = downsample_image(src, 4096, 4096, 4, &dw, &dh, 0.25f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_checkerboard_100x100(void) {
uint8_t *src = create_checkerboard(100, 100, 10);
int dw, dh;
for (int i = 0; i < 200; i++) {
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
static double bench_downsample_checkerboard_256x256(void) {
uint8_t *src = create_checkerboard(256, 256, 16);
int dw, dh;
for (int i = 0; i < 100; i++) {
uint8_t *dst = downsample_image(src, 256, 256, 4, &dw, &dh, 0.5f);
free(dst);
}
free(src);
return 0;
}
typedef struct {
const char *name;
double (*fn)(void);
} BenchDef;
static BenchDef benchmarks[] = {
{"create_uniform_16x16", bench_create_uniform_16x16},
{"create_gradient_256x256", bench_create_gradient_256x256},
{"create_noise_1024x1024", bench_create_noise_1024x1024},
{"downsample_uniform_16x16_0.5x", bench_downsample_uniform_16x16},
{"downsample_gradient_64x64_0.5x", bench_downsample_gradient_64x64},
{"downsample_gradient_256x256_0.5x", bench_downsample_gradient_256x256},
{"downsample_gradient_1024x1024_0.5x", bench_downsample_gradient_1024x1024},
{"downsample_noise_512x512_0.25x", bench_downsample_noise_512x512},
{"downsample_noise_1024x1024_0.25x", bench_downsample_noise_1024x1024},
{"downsample_noise_1920x1080_0.5x", bench_downsample_noise_1920x1080},
{"downsample_noise_3840x2160_0.5x", bench_downsample_noise_3840x2160},
{"downsample_noise_4096x4096_0.25x", bench_downsample_noise_4096x4096},
{"downsample_checkerboard_100x100_0.5x", bench_downsample_checkerboard_100x100},
{"downsample_checkerboard_256x256_0.5x", bench_downsample_checkerboard_256x256},
};
static int benchmark_iterations[] = {
5000, 500, 50, 2000, 500, 200, 20, 50, 15, 5, 2, 2, 200, 100
};
int main(int argc, char **argv) {
int csv_output = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--csv") == 0) {
csv_output = 1;
}
}
if (csv_output) {
printf("name,time_ms,pixels_per_sec,megabytes_per_sec,iterations\n");
} else {
printf("Chroma Performance Benchmarks\n");
printf("=============================\n\n");
printf(" %-42s %16s %22s %19s\n", "Benchmark", "Time (ms)", "Pixels/sec", "MB/sec");
printf(" %-42s %16s %22s %19s\n", "-----------------------------------------", "--------------", "-----------------", "--------------");
}
int num_benchmarks = sizeof(benchmarks) / sizeof(benchmarks[0]);
int max_name_len = 0;
for (int i = 0; i < num_benchmarks; i++) {
int len = strlen(benchmarks[i].name);
if (len > max_name_len) max_name_len = len;
}
for (int i = 0; i < num_benchmarks; i++) {
BenchResult result = {0};
calculate_bench_metrics(benchmarks[i].name, benchmark_iterations[i], &result);
double elapsed_ms;
run_bench(benchmarks[i].fn, benchmark_iterations[i], &elapsed_ms);
size_t total_pixels = result.input_bytes > 0 ? result.input_bytes / 4 : 0;
result.time_ms = elapsed_ms;
if (total_pixels > 0 && elapsed_ms > 0) {
result.pixels_per_sec = total_pixels / (elapsed_ms / 1000.0);
}
double total_mb = (result.input_bytes + result.output_bytes) / (1024.0 * 1024.0);
if (total_mb > 0 && elapsed_ms > 0) {
result.megabytes_per_sec = total_mb / (elapsed_ms / 1000.0);
}
if (csv_output) {
printf("%s,%.3f,%.0f,%.2f,%d\n",
benchmarks[i].name, result.time_ms, result.pixels_per_sec,
result.megabytes_per_sec, benchmark_iterations[i]);
} else {
printf(" %-42s %16.3f %22.0f %19.2f\n",
benchmarks[i].name, result.time_ms,
result.pixels_per_sec, result.megabytes_per_sec);
}
}
if (!csv_output) {
printf("\n");
}
(void)argc;
return 0;
}

View file

@ -52,7 +52,7 @@ 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
@ -73,6 +73,21 @@ min_scale_factor = 0.25 # Don't scale below 25% of original size
# ==================================
# 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
@ -90,23 +105,6 @@ min_scale_factor = 0.25 # Don't scale below 25% of original size
# bottom-left - image anchored to bottom-left corner
# bottom-right - image anchored to bottom-right corner
#
# To find your output names, run one of these commands:
#
# 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)
# - eDP-1 (embedded DisplayPort, laptops)
# - DVI-D-1, DVI-I-1 (DVI)
# - VGA-1 (VGA, legacy)
#
# Examples:
# output.HDMI-A-1 = ~/Pictures/wallpaper.jpg
# output.DP-1 = ~/Pictures/monitor1.png
@ -115,6 +113,14 @@ min_scale_factor = 0.25 # Don't scale below 25% of original size
# 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

8
include/chroma.h vendored
View file

@ -250,11 +250,13 @@ void chroma_images_cleanup(chroma_state_t *state);
int chroma_config_load(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);
const char *output_name,
const char *output_description);
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,
chroma_anchor_t *anchor, float *anchor_x, float *anchor_y);
const char *output_description, chroma_scale_mode_t *scale_mode,
chroma_filter_quality_t *filter_quality, chroma_anchor_t *anchor,
float *anchor_x, float *anchor_y);
void chroma_config_print(const chroma_config_t *config);

190
lib/test_common.c Normal file
View file

@ -0,0 +1,190 @@
#define STB_IMAGE_IMPLEMENTATION
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "test_common.h"
#include "stb_image.h"
#include "stb_image_write.h"
#include <math.h>
#include <sys/time.h>
uint8_t *load_image(const char *path, int *width, int *height, int *channels) {
return stbi_load(path, width, height, channels, 4);
}
int save_image(const char *path, uint8_t *data, int width, int height, int channels) {
return stbi_write_png(path, width, height, channels, data, width * channels);
}
uint8_t *downsample_image(uint8_t *src, int sw, int sh, int sc, int *dw, int *dh, float scale) {
if (!src || sw <= 0 || sh <= 0 || sc <= 0 || scale <= 0) {
if (dw) *dw = 0;
if (dh) *dh = 0;
return NULL;
}
int tw = (int)(sw * scale);
int th = (int)(sh * scale);
if (tw < 1) tw = 1;
if (th < 1) th = 1;
uint8_t *dst = malloc(tw * th * sc);
if (!dst) return NULL;
float inv_scale = 1.0f / scale;
for (int y = 0; y < th; y++) {
for (int x = 0; x < tw; x++) {
float sx = (x + 0.5f) * inv_scale;
float sy = (y + 0.5f) * inv_scale;
int ix = (int)sx;
int iy = (int)sy;
if (sw > 1) {
ix = (ix < sw - 2) ? ix : sw - 2;
} else {
ix = 0;
}
if (sh > 1) {
iy = (iy < sh - 2) ? iy : sh - 2;
} else {
iy = 0;
}
if (ix < 0) ix = 0;
if (iy < 0) iy = 0;
float fx = sx - ix;
float fy = sy - iy;
if (sw == 1) fx = 0.0f;
if (sh == 1) fy = 0.0f;
for (int c = 0; c < sc; c++) {
uint8_t p00 = src[(iy * sw + ix) * sc + c];
uint8_t p01 = (sw > 1) ? src[(iy * sw + ix + 1) * sc + c] : p00;
uint8_t p10 = (sh > 1) ? src[((iy + 1) * sw + ix) * sc + c] : p00;
uint8_t p11 = (sw > 1 && sh > 1) ? src[((iy + 1) * sw + ix + 1) * sc + c] : p00;
float interp = p00 * (1 - fx) * (1 - fy) +
p01 * fx * (1 - fy) +
p10 * (1 - fx) * fy +
p11 * fx * fy;
dst[(y * tw + x) * sc + c] = (uint8_t)(interp + 0.5f);
}
}
}
*dw = tw;
*dh = th;
return dst;
}
double get_time_ms(void) {
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * 1000.0 + tv.tv_usec / 1000.0;
}
double run_benchmark(double (*fn)(void), int iterations) {
double warmup = fn();
(void)warmup;
double total = 0.0;
for (int i = 0; i < iterations; i++) {
total += fn();
}
return total / iterations;
}
int compare_images(uint8_t *a, uint8_t *b, int w, int h, int ch, float threshold) {
int max_diff = 0;
for (int i = 0; i < w * h * ch; i++) {
int diff = abs((int)a[i] - (int)b[i]);
if (diff > max_diff) max_diff = diff;
if (diff > (int)(threshold * 255.0f)) {
return 0;
}
}
return 1;
}
float compute_psnr(uint8_t *a, uint8_t *b, int w, int h, int ch) {
double mse = 0.0;
int total = w * h * ch;
for (int i = 0; i < total; i++) {
double diff = (double)a[i] - (double)b[i];
mse += diff * diff;
}
mse /= total;
if (mse < 1e-10) return 99.99f;
return (float)(10.0 * log10(255.0 * 255.0 / mse));
}
uint8_t *create_gradient_image(int w, int h) {
uint8_t *img = malloc(w * h * 4);
if (!img) return NULL;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int idx = (y * w + x) * 4;
img[idx + 0] = (uint8_t)((float)x / w * 255);
img[idx + 1] = (uint8_t)((float)y / h * 255);
img[idx + 2] = 128;
img[idx + 3] = 255;
}
}
return img;
}
uint8_t *create_noise_image(int w, int h, unsigned int seed) {
uint8_t *img = malloc(w * h * 4);
if (!img) return NULL;
srand(seed);
for (int i = 0; i < w * h * 4; i++) {
img[i] = (uint8_t)(rand() % 256);
}
return img;
}
uint8_t *create_uniform_image(int w, int h, uint8_t r, uint8_t g, uint8_t b) {
uint8_t *img = malloc(w * h * 4);
if (!img) return NULL;
for (int i = 0; i < w * h; i++) {
img[i * 4 + 0] = r;
img[i * 4 + 1] = g;
img[i * 4 + 2] = b;
img[i * 4 + 3] = 255;
}
return img;
}
uint8_t *create_checkerboard(int w, int h, int check_size) {
uint8_t *img = malloc(w * h * 4);
if (!img) return NULL;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int idx = (y * w + x) * 4;
int cx = x / check_size;
int cy = y / check_size;
if ((cx + cy) % 2 == 0) {
img[idx + 0] = 255;
img[idx + 1] = 255;
img[idx + 2] = 255;
} else {
img[idx + 0] = 0;
img[idx + 1] = 0;
img[idx + 2] = 0;
}
img[idx + 3] = 255;
}
}
return img;
}

View file

@ -15,7 +15,7 @@
in
stdenv.mkDerivation {
pname = "chroma";
version = "1.0.0";
version = "1.0.1";
src = fs.toSource {
root = s;
@ -23,8 +23,11 @@ in
(s + /include)
(s + /protocols)
(s + /src)
(s + /Makefile)
# For testing
(s + /lib)
(s + /tests)
];
};
@ -49,9 +52,17 @@ in
makeFlags = [
"PREFIX=$(out)"
"SYSTEMD_DIR=$(out)/lib/systemd/system"
"SYSTEMD_DIR=$(out)/lib/systemd/system" # FIXME: this is an user service, actually
];
checkPhase = ''
runHook preCheck
make test
runHook postCheck
'';
postInstall = ''
install -Dm755 ${../chroma.conf.sample} $out/share/chroma.conf.sample
'';

378
scripts/generate_report.py Normal file
View file

@ -0,0 +1,378 @@
#!/usr/bin/env python3
import csv
import os
import sys
from datetime import datetime
from pathlib import Path
try:
import matplotlib.pyplot as plt
import numpy as np
HAS_MATPLOTLIB = True
except ImportError:
HAS_MATPLOTLIB = False
CSV_DIR = Path("/tmp")
OUTPUT_DIR = Path("/tmp")
RESOLUTIONS = ["1080p", "1440p", "4K", "5K", "6K", "8K"]
SCENARIOS = [
("No_Downsampling", "No Downsampling"),
("1080p_Target", "1080p Target"),
("1440p_Target", "1440p Target"),
("4K_Target", "4K Target"),
]
def load_csv_data(filename: str) -> list[dict]:
"""Load data from CSV file."""
data = []
with open(filename, "r") as f:
reader = csv.DictReader(f)
for row in reader:
data.append(row)
return data
def extract_value(csv_file: str, resolution: str, column: str) -> str | None:
"""Extract a value from CSV for a given resolution and column."""
if not os.path.exists(csv_file):
return None
data = load_csv_data(csv_file)
for row in data:
if row.get("Resolution") == resolution:
return row.get(column)
return None
def generate_text_report() -> str:
"""Generate a text-based report."""
lines = []
lines.append("Chroma Memory Impact Analysis Report")
lines.append("=" * 44)
lines.append(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
lines.append("")
lines.append("=== Memory Usage Summary ===")
lines.append("")
lines.append(
f"{'Input':<8} {'Original':<12} {'Downsampled':<12} {'Savings':<10} {'Downsampled?':<12}"
)
lines.append(f"{'Res':<8} {'Size (MB)':<12} {'Size (MB)':<12} {'(%)':<10} {'':12}")
lines.append("-" * 56)
for res in RESOLUTIONS:
original = extract_value(
str(CSV_DIR / "chroma_memory_No_Downsampling.csv"), res, "OriginalSizeMB"
)
downsampled = extract_value(
str(CSV_DIR / "chroma_memory_4K_Target.csv"), res, "DownsampledSizeMB"
)
savings = extract_value(
str(CSV_DIR / "chroma_memory_4K_Target.csv"), res, "MemorySavingsPercent"
)
if original:
orig_mb = float(original)
down_mb = float(downsampled) if downsampled else orig_mb
sav_pct = float(savings) if savings else 0.0
downsampled_yes = "Yes" if sav_pct > 0 else "No"
lines.append(
f"{res:<8} {orig_mb:<12.2f} {down_mb:<12.2f} {sav_pct:<10.1f} {downsampled_yes:<12}"
)
lines.append("")
lines.append("=== Key Findings ===")
lines.append("")
lines.append("Memory Savings by Scenario (4K images):")
lines.append("")
for name, display_name in SCENARIOS[1:]:
csv_path = CSV_DIR / f"chroma_memory_{name}.csv"
savings = extract_value(str(csv_path), "4K", "MemorySavingsPercent")
if savings:
lines.append(f" {display_name:<20}: {float(savings):>6.1f}%")
lines.append("")
lines.append("=== Impact on Typical Usage ===")
lines.append("")
lines.append("Scenario: User with 5 wallpapers, mixed resolutions")
lines.append("")
lines.append("Without downsampling: 5 × 31.6 MB = 158.2 MB")
lines.append("With 4K target: 5 × 7.9 MB = 39.6 MB")
lines.append("Memory saved: 118.6 MB (75.0%)")
lines.append("")
lines.append("=== Recommendations ===")
lines.append("")
lines.append("1. Enable downsampling for systems with < 8GB RAM")
lines.append("2. Use 4K target for most users (good balance)")
lines.append("3. Use 1080p target for low-memory systems")
lines.append("4. Disable downsampling only for systems with > 16GB RAM")
lines.append("5. Adjust min_scale_factor to preserve detail when needed")
lines.append("")
lines.append("=== Configuration Examples ===")
lines.append("")
lines.append("# Maximum Performance (low memory)")
lines.append("enable_downsampling = true")
lines.append("max_output_width = 1920")
lines.append("max_output_height = 1080")
lines.append("min_scale_factor = 0.5")
lines.append("")
lines.append("# Balanced (default)")
lines.append("enable_downsampling = true")
lines.append("max_output_width = 3840")
lines.append("max_output_height = 2160")
lines.append("min_scale_factor = 0.25")
lines.append("")
lines.append("# Maximum Quality")
lines.append("enable_downsampling = false")
lines.append("")
lines.append("=== Raw Data ===")
lines.append("")
lines.append("CSV files available at:")
csv_files = list(CSV_DIR.glob("chroma_memory_*.csv"))
if csv_files:
for f in sorted(csv_files):
lines.append(f" {f}")
else:
lines.append(" No CSV files found")
lines.append("")
lines.append("Run 'make profile-memory' to regenerate data.")
return "\n".join(lines)
def create_memory_comparison_graph():
"""Create memory comparison graph for all scenarios."""
if not HAS_MATPLOTLIB:
raise ImportError("matplotlib not available")
plt.figure(figsize=(12, 8))
colors = ["#FF6B6B", "#4ECDC4", "#45B7D1", "#96CEB4"]
patterns = ["/", "\\", "|", "-"]
x = np.arange(len(RESOLUTIONS))
width = 0.2
for i, (scenario_key, scenario_name) in enumerate(SCENARIOS):
csv_path = CSV_DIR / f"chroma_memory_{scenario_key}.csv"
if csv_path.exists():
data = load_csv_data(str(csv_path))
original_sizes = []
downsampled_sizes = []
for res in RESOLUTIONS:
row = next((r for r in data if r.get("Resolution") == res), None)
if row:
original_sizes.append(float(row.get("OriginalSizeMB", 0)))
downsampled_sizes.append(float(row.get("DownsampledSizeMB", 0)))
else:
original_sizes.append(0)
downsampled_sizes.append(0)
offset = i * width
plt.bar(
x + offset,
original_sizes,
width,
label=f"{scenario_name} - Original",
color=colors[i],
alpha=0.7,
)
plt.bar(
x + offset,
downsampled_sizes,
width,
label=f"{scenario_name} - Downsampled",
color=colors[i],
alpha=0.9,
hatch=patterns[i],
)
plt.xlabel("Input Resolution")
plt.ylabel("Memory Usage (MB)")
plt.title("Chroma Memory Usage: Original vs Downsampled")
plt.xticks(x + width * 1.5, RESOLUTIONS)
plt.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig(
OUTPUT_DIR / "chroma_memory_comparison.png", dpi=300, bbox_inches="tight"
)
plt.close()
def create_savings_graph():
"""Create memory savings percentage graph."""
if not HAS_MATPLOTLIB:
raise ImportError("matplotlib not available")
plt.figure(figsize=(10, 6))
colors = ["#FF6B6B", "#4ECDC4", "#45B7D1"]
markers = ["o", "s", "^"]
for i, (scenario_key, scenario_name) in enumerate(SCENARIOS[1:]):
csv_path = CSV_DIR / f"chroma_memory_{scenario_key}.csv"
if csv_path.exists():
data = load_csv_data(str(csv_path))
resolutions = []
savings = []
for row in data:
pct = row.get("MemorySavingsPercent", "0")
try:
if float(pct) > 0:
resolutions.append(row.get("Resolution", ""))
savings.append(float(pct))
except ValueError:
continue
plt.plot(
resolutions,
savings,
marker=markers[i],
color=colors[i],
linewidth=2,
markersize=8,
label=scenario_name,
)
plt.xlabel("Input Resolution")
plt.ylabel("Memory Savings (%)")
plt.title("Memory Savings by Input Resolution and Target")
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.savefig(OUTPUT_DIR / "chroma_savings.png", dpi=300, bbox_inches="tight")
plt.close()
def create_summary_table():
"""Create a summary table image."""
if not HAS_MATPLOTLIB:
raise ImportError("matplotlib not available")
fig, ax = plt.subplots(figsize=(10, 6))
ax.axis("tight")
ax.axis("off")
scenario_names = [name for _, name in SCENARIOS]
table_data = []
for res in RESOLUTIONS:
row = [res]
for scenario_key, _ in SCENARIOS:
csv_path = CSV_DIR / f"chroma_memory_{scenario_key}.csv"
if csv_path.exists():
data = load_csv_data(str(csv_path))
data_row = next((r for r in data if r.get("Resolution") == res), None)
if data_row:
savings = data_row.get("MemorySavingsPercent", "0")
try:
pct = float(savings)
row.append(f"{pct:.1f}%" if pct > 0 else "No change")
except ValueError:
row.append("N/A")
else:
row.append("N/A")
else:
row.append("N/A")
table_data.append(row)
columns = ["Resolution"] + scenario_names
table = ax.table(
cellText=table_data, colLabels=columns, cellLoc="center", loc="center"
)
table.auto_set_font_size(False)
table.set_fontsize(10)
table.scale(1.2, 1.5)
for i in range(len(columns)):
table[(0, i)].set_facecolor("#40466e")
table[(0, i)].set_text_props(weight="bold", color="white")
plt.title("Memory Savings Summary Table", fontsize=14, pad=20)
plt.savefig(OUTPUT_DIR / "chroma_summary_table.png", dpi=300, bbox_inches="tight")
plt.close()
def check_csv_files() -> list[str]:
"""Check which CSV files exist."""
missing = []
for name, _ in SCENARIOS:
csv_path = CSV_DIR / f"chroma_memory_{name}.csv"
if not csv_path.exists():
missing.append(str(csv_path))
return missing
def main():
import argparse
global OUTPUT_DIR
parser = argparse.ArgumentParser(
description="Chroma Memory Analysis Report Generator"
)
parser.add_argument(
"--text", action="store_true", help="Generate text report to stdout"
)
parser.add_argument("--graphs", action="store_true", help="Generate PNG graphs")
parser.add_argument(
"--all",
action="store_true",
help="Generate both text report and graphs (default)",
)
parser.add_argument(
"--output-dir",
type=str,
default=str(OUTPUT_DIR),
help=f"Output directory (default: {OUTPUT_DIR})",
)
args = parser.parse_args()
do_text = args.text or args.all or not (args.text or args.graphs)
do_graphs = args.graphs or args.all
OUTPUT_DIR = Path(args.output_dir)
missing = check_csv_files()
if missing and (do_text or do_graphs):
print("Missing CSV files:")
for f in missing:
print(f" {f}")
print("\nRun 'make profile-memory' first to generate CSV files.")
sys.exit(1)
if do_text:
print(generate_text_report())
if do_graphs:
if not HAS_MATPLOTLIB:
print("Error: matplotlib not found.")
print("Install with: pip install matplotlib numpy")
sys.exit(1)
print("\nGenerating graphs...")
try:
create_memory_comparison_graph()
print(f" Created: {OUTPUT_DIR / 'chroma_memory_comparison.png'}")
create_savings_graph()
print(f" Created: {OUTPUT_DIR / 'chroma_savings.png'}")
create_summary_table()
print(f" Created: {OUTPUT_DIR / 'chroma_summary_table.png'}")
print("\nGraph generation complete!")
except Exception as e:
print(f"Error generating graphs: {e}")
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -8,6 +8,7 @@ pkgs.mkShell {
gdb
valgrind
strace
bear
# Code formatting and analysis
clang-tools # includes clang-format

View file

@ -3,6 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "../include/chroma.h"
@ -27,6 +28,38 @@ static char *trim_whitespace(char *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"
// - Description prefix match: "desc:Samsung" matches if description starts
// with "Samsung"
static bool match_output(const char *pattern, const char *output_name,
const char *output_description) {
if (!pattern || !output_name) {
return false;
}
// Try exact name match first
if (strcmp(pattern, output_name) == 0) {
return true;
}
// Check for description prefix match: "desc:<prefix>"
if (strncmp(pattern, "desc:", 5) == 0) {
const char *desc_prefix = pattern + 5;
size_t prefix_len = strlen(desc_prefix);
if (output_description && prefix_len > 0) {
// Match if description starts with the prefix (case-insensitive)
if (strncasecmp(output_description, desc_prefix, prefix_len) == 0) {
return true;
}
}
}
return false;
}
static char *remove_quotes(char *str) {
size_t len = strlen(str);
if (len >= 2 && ((str[0] == '"' && str[len - 1] == '"') ||
@ -196,7 +229,7 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
return CHROMA_ERROR_MEMORY;
}
// Validate string lengths to prevent buffer overflow
// XXX: Validate string lengths to prevent buffer overflow
size_t output_len = strlen(output_name);
size_t path_len = strlen(image_path);
@ -229,7 +262,7 @@ static int add_output_mapping(chroma_config_t *config, const char *output_name,
"Added mapping: %s -> %s (scale: %s, filter: %s, anchor: %s @ %.1f,%.1f)",
output_name, image_path, scale_mode_to_string(scale_mode),
filter_quality_to_string(filter_quality), anchor_to_string(anchor),
anchor_x, anchor_y);
(double)anchor_x, (double)anchor_y);
chroma_log("TRACE", "Output mapping %d: '%s' -> '%s' (path length: %zu)",
config->mapping_count, output_name, image_path, path_len);
return CHROMA_OK;
@ -370,7 +403,7 @@ static int parse_config_line(chroma_config_t *config, char *line,
config->default_anchor_x = 50.0f;
} else {
config->default_anchor_x = ax;
chroma_log("DEBUG", "Set default anchor_x: %.1f", ax);
chroma_log("DEBUG", "Set default anchor_x: %.1f", (double)ax);
}
} else if (strcasecmp(key, "anchor_y") == 0) {
char *endptr = NULL;
@ -384,7 +417,7 @@ static int parse_config_line(chroma_config_t *config, char *line,
config->default_anchor_y = 50.0f;
} else {
config->default_anchor_y = ay;
chroma_log("DEBUG", "Set default anchor_y: %.1f", ay);
chroma_log("DEBUG", "Set default anchor_y: %.1f", (double)ay);
}
} else if (strcasecmp(key, "max_output_width") == 0) {
int width = atoi(value);
@ -403,10 +436,10 @@ static int parse_config_line(chroma_config_t *config, char *line,
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
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", factor);
chroma_log("DEBUG", "Set minimum scale factor: %.2f", (double)factor);
} else {
chroma_log("WARN", "Invalid min_scale_factor: %s (using 0.25)", value);
}
@ -419,9 +452,9 @@ static int parse_config_line(chroma_config_t *config, char *line,
return CHROMA_OK;
}
// Check for extended output configuration with properties
// Format: output.DP-1.scale = fill
// Format: output.DP-1.filter = linear
// 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)
@ -494,24 +527,24 @@ static int parse_config_line(chroma_config_t *config, char *line,
}
chroma_log("DEBUG", "Set anchor for output %s: %s (x=%.1f, y=%.1f)",
output_name, anchor_to_string(mapping->anchor),
mapping->anchor_x, mapping->anchor_y);
(double)mapping->anchor_x, (double)mapping->anchor_y);
} else if (strcasecmp(property, "anchor_x") == 0) {
float ax = atof(value);
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,
ax);
(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 = atof(value);
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,
ay);
(double)ay);
} else {
mapping->anchor_y = 50.0f;
chroma_log("WARN", "Invalid anchor_y: %s (range 0-100, using 50)",
@ -627,7 +660,7 @@ int chroma_config_load(chroma_config_t *config, const char *config_file) {
// Log configuration memory usage
size_t config_size =
sizeof(chroma_config_t) +
(config->mapping_count * sizeof(chroma_config_mapping_t));
((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
chroma_log_resource_allocation("config_data", config_size,
"configuration structure");
chroma_log_memory_stats("post-config-load");
@ -644,7 +677,7 @@ void chroma_config_free(chroma_config_t *config) {
// Log configuration deallocation
size_t config_size =
sizeof(chroma_config_t) +
(config->mapping_count * sizeof(chroma_config_mapping_t));
((size_t)config->mapping_count * sizeof(chroma_config_mapping_t));
chroma_log_resource_deallocation("config_data", config_size,
"configuration structure");
@ -660,16 +693,19 @@ void chroma_config_free(chroma_config_t *config) {
// Get image path for specific output
const char *chroma_config_get_image_for_output(chroma_config_t *config,
const char *output_name) {
const char *output_name,
const char *output_description) {
if (!config || !output_name) {
return NULL;
}
// Look for specific output mapping
// Look for specific output mapping (name or description match)
for (int i = 0; i < config->mapping_count; i++) {
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
chroma_log("DEBUG", "Found specific mapping for output %s: %s",
output_name, config->mappings[i].image_path);
if (match_output(config->mappings[i].output_name, output_name,
output_description)) {
chroma_log("DEBUG", "Found specific mapping for output %s (desc: %s): %s",
output_name, output_description ? output_description : "none",
config->mappings[i].image_path);
return config->mappings[i].image_path;
}
}
@ -689,27 +725,31 @@ const char *chroma_config_get_image_for_output(chroma_config_t *config,
// quality, anchor, and custom anchor coordinates
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,
chroma_anchor_t *anchor, float *anchor_x, float *anchor_y) {
const char *output_description, 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 || !scale_mode || !filter_quality || !anchor ||
!anchor_x || !anchor_y) {
return CHROMA_ERROR_INIT;
}
// Look for specific output mapping
// Look for specific output mapping (name or description match)
for (int i = 0; i < config->mapping_count; i++) {
if (strcmp(config->mappings[i].output_name, output_name) == 0) {
if (match_output(config->mappings[i].output_name, output_name,
output_description)) {
*scale_mode = config->mappings[i].scale_mode;
*filter_quality = config->mappings[i].filter_quality;
*anchor = config->mappings[i].anchor;
*anchor_x = config->mappings[i].anchor_x;
*anchor_y = config->mappings[i].anchor_y;
chroma_log("DEBUG",
"Found specific mapping for output %s: scale=%s, filter=%s, "
"anchor=%s @ %.1f,%.1f",
output_name, scale_mode_to_string(*scale_mode),
"Found specific mapping for output %s (desc: %s): scale=%s, "
"filter=%s, anchor=%s @ %.1f,%.1f",
output_name, output_description ? output_description : "none",
scale_mode_to_string(*scale_mode),
filter_quality_to_string(*filter_quality),
anchor_to_string(*anchor), *anchor_x, *anchor_y);
anchor_to_string(*anchor), (double)*anchor_x,
(double)*anchor_y);
return CHROMA_OK;
}
}
@ -725,7 +765,7 @@ int chroma_config_get_mapping_for_output(
"%.1f,%.1f",
output_name, scale_mode_to_string(*scale_mode),
filter_quality_to_string(*filter_quality),
anchor_to_string(*anchor), *anchor_x, *anchor_y);
anchor_to_string(*anchor), (double)*anchor_x, (double)*anchor_y);
return CHROMA_OK;
}
@ -747,7 +787,8 @@ void chroma_config_print(const chroma_config_t *config) {
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", "Min scale factor: %.2f",
(double)config->min_scale_factor);
}
chroma_log("INFO", "Output mappings: %d", config->mapping_count);

View file

@ -77,7 +77,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
// Get image path for this output
const char *image_path = chroma_config_get_image_for_output(
&state->config, output->name ? output->name : "unknown");
&state->config, output->name ? output->name : "unknown",
output->description);
if (!image_path) {
chroma_log("WARN", "No wallpaper configured for output %u (%s)", output->id,
output->name ? output->name : "unknown");
@ -121,14 +122,15 @@ static int assign_wallpaper_to_output(chroma_state_t *state,
// anchor coords)
if (chroma_config_get_mapping_for_output(
&state->config, output->name ? output->name : "unknown",
&output->scale_mode, &output->filter_quality, &output->anchor,
&output->anchor_x, &output->anchor_y) == CHROMA_OK) {
output->description, &output->scale_mode, &output->filter_quality,
&output->anchor, &output->anchor_x, &output->anchor_y) == CHROMA_OK) {
output->config_loaded = true;
chroma_log("DEBUG",
"Loaded config for output %u: scale=%d, filter=%d, anchor=%d @ "
"%.1f,%.1f",
output->id, output->scale_mode, output->filter_quality,
output->anchor, output->anchor_x, output->anchor_y);
output->anchor, (double)output->anchor_x,
(double)output->anchor_y);
// Check if configuration changed and invalidate texture if needed
if (had_config &&

View file

@ -1,5 +1,5 @@
#define STB_IMAGE_IMPLEMENTATION
#include "../include/stb_image.h"
#include "../include/vendor/stb_image.h"
#include <stdio.h>
#include <stdlib.h>
@ -37,16 +37,16 @@ static void calculate_optimal_size(int original_width, int original_height,
}
// 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_x = (float)max_output_width / (float)original_width;
float scale_y = (float)max_output_height / (float)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);
*optimal_width = (int)((float)original_width * scale);
*optimal_height = (int)((float)original_height * scale);
// Ensure even dimensions for better GPU alignment
*optimal_width = (*optimal_width / 2) * 2;
@ -64,14 +64,14 @@ static int downsample_image(unsigned char *src_data, int src_width,
return -1;
}
float x_ratio = (float)src_width / dst_width;
float y_ratio = (float)src_height / dst_height;
float x_ratio = (float)src_width / (float)dst_width;
float y_ratio = (float)src_height / (float)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);
int src_x = (int)((float)x * x_ratio);
int src_y = (int)((float)y * y_ratio);
// Ensure we're within bounds
src_x = (src_x >= src_width) ? src_width - 1 : src_x;
@ -160,14 +160,14 @@ int chroma_image_load(chroma_image_t *image, const char *path,
&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_x = (float)optimal_width / (float)original_width;
float scale_y = (float)optimal_height / (float)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);
optimal_width = (int)((float)original_width * scale);
optimal_height = (int)((float)original_height * scale);
// Ensure even dimensions
optimal_width = (optimal_width / 2) * 2;
@ -180,13 +180,14 @@ int chroma_image_load(chroma_image_t *image, const char *path,
// Downsamp if needed and enabled
if (should_downsample) {
double reduction_ratio = (double)(optimal_width * optimal_height) /
(double)(original_width * original_height) * 100.0;
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);
reduction_ratio);
size_t optimal_size = (size_t)optimal_width * optimal_height * 4;
size_t optimal_size = (size_t)optimal_width * (size_t)optimal_height * 4;
unsigned char *downsampled_data = malloc(optimal_size);
if (!downsampled_data) {
chroma_log("ERROR", "Failed to allocate memory for downsampled image");
@ -219,7 +220,8 @@ int chroma_image_load(chroma_image_t *image, const char *path,
image->loaded = true;
// Calculate and log memory allocation
size_t image_size = (size_t)image->width * image->height * image->channels;
size_t image_size =
(size_t)image->width * (size_t)image->height * (size_t)image->channels;
chroma_log_resource_allocation("image_data", image_size, path);
chroma_log("INFO", "Loaded image: %s (%dx%d, %d channels, %.2f MB)%s", path,
@ -238,7 +240,8 @@ void chroma_image_free(chroma_image_t *image) {
if (image->data) {
// Log memory deallocation before freeing
size_t image_size = (size_t)image->width * image->height * image->channels;
size_t image_size =
(size_t)image->width * (size_t)image->height * (size_t)image->channels;
if (strlen(image->path) > 0) {
chroma_log("DEBUG", "Freed image: %s", image->path);

View file

@ -6,7 +6,7 @@
#include <GLES2/gl2.h>
#include "../include/chroma.h"
#include "../include/stb_image.h"
#include "../include/vendor/stb_image.h"
// Convert filter quality enum to OpenGL parameters
static void get_gl_filter_params(chroma_filter_quality_t quality,
@ -64,14 +64,14 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
// Center image at original size
// Calculate how much of the texture to show
{
float image_aspect = (float)image_width / image_height;
float output_aspect = (float)output_width / output_height;
float image_aspect = (float)image_width / (float)image_height;
float output_aspect = (float)output_width / (float)output_height;
if (image_aspect > output_aspect) {
// Image is wider - fit width, show center portion vertically
float visible_height = (float)image_width / output_aspect;
float v_offset =
(image_height - visible_height) / (2.0f * image_height);
float v_offset = ((float)image_height - visible_height) /
(2.0f * (float)image_height);
u1 = 0.0f;
v1 = v_offset;
u2 = 1.0f;
@ -79,7 +79,8 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
} else {
// Image is taller - fit height, show center portion horizontally
float visible_width = (float)image_height * output_aspect;
float u_offset = (image_width - visible_width) / (2.0f * image_width);
float u_offset =
((float)image_width - visible_width) / (2.0f * (float)image_width);
u1 = u_offset;
v1 = 0.0f;
u2 = 1.0f - u_offset;
@ -91,14 +92,14 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
case CHROMA_SCALE_FIT:
// Fit image within output, maintaining aspect ratio
{
float image_aspect = (float)image_width / image_height;
float output_aspect = (float)output_width / output_height;
float image_aspect = (float)image_width / (float)image_height;
float output_aspect = (float)output_width / (float)output_height;
if (image_aspect > output_aspect) {
// Image is wider - fit width, add borders top/bottom
float scaled_height = (float)output_width / image_aspect;
float v_border =
(output_height - scaled_height) / (2.0f * output_height);
float v_border = ((float)output_height - scaled_height) /
(2.0f * (float)output_height);
u1 = 0.0f;
v1 = v_border;
u2 = 1.0f;
@ -106,7 +107,8 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
} else {
// Image is taller - fit height, add borders left/right
float scaled_width = (float)output_height * image_aspect;
float u_border = (output_width - scaled_width) / (2.0f * output_width);
float u_border =
((float)output_width - scaled_width) / (2.0f * (float)output_width);
u1 = u_border;
v1 = 0.0f;
u2 = 1.0f - u_border;
@ -119,21 +121,23 @@ static void calculate_texture_coords(chroma_scale_mode_t scale_mode,
default:
// Fill entire output, crop if necessary
{
float image_aspect = (float)image_width / image_height;
float output_aspect = (float)output_width / output_height;
float image_aspect = (float)image_width / (float)image_height;
float output_aspect = (float)output_width / (float)output_height;
if (image_aspect > output_aspect) {
// Image is wider - crop left/right
float crop_width = image_height * output_aspect;
float u_crop = (image_width - crop_width) / (2.0f * image_width);
float crop_width = (float)image_height * output_aspect;
float u_crop =
((float)image_width - crop_width) / (2.0f * (float)image_width);
u1 = u_crop;
v1 = 0.0f;
u2 = 1.0f - u_crop;
v2 = 1.0f;
} else {
// Image is taller - crop top/bottom
float crop_height = image_width / output_aspect;
float v_crop = (image_height - crop_height) / (2.0f * image_height);
float crop_height = (float)image_width / output_aspect;
float v_crop =
((float)image_height - crop_height) / (2.0f * (float)image_height);
u1 = 0.0f;
v1 = v_crop;
u2 = 1.0f;
@ -336,7 +340,8 @@ static int update_texture_from_image(chroma_output_t *output,
// Could this b made more accurate?
if (output->image && output->image->loaded) {
size_t texture_size = (size_t)output->image->width *
output->image->height * output->image->channels;
(size_t)output->image->height *
(size_t)output->image->channels;
chroma_log_resource_deallocation("gpu_texture", texture_size,
"texture replacement");
}
@ -349,7 +354,8 @@ static int update_texture_from_image(chroma_output_t *output,
glBindTexture(GL_TEXTURE_2D, output->texture_id);
// Log GPU texture allocation
size_t texture_size = (size_t)image->width * image->height * image->channels;
size_t texture_size =
(size_t)image->width * (size_t)image->height * (size_t)image->channels;
chroma_log_resource_allocation("gpu_texture", texture_size, image->path);
// Set texture parameters
@ -398,8 +404,8 @@ static int update_texture_from_image(chroma_output_t *output,
// 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 * image->height * image->channels;
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",
@ -580,8 +586,8 @@ int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
}
// Configure layer surface
zwlr_layer_surface_v1_set_size(output->layer_surface, output->width,
output->height);
zwlr_layer_surface_v1_set_size(output->layer_surface, (uint32_t)output->width,
(uint32_t)output->height);
zwlr_layer_surface_v1_set_anchor(output->layer_surface,
ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP |
ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT |
@ -626,8 +632,7 @@ int chroma_surface_create(chroma_state_t *state, chroma_output_t *output) {
output->width, output->height);
// Log surface creation resource allocation
size_t surface_size =
(size_t)output->width * output->height * 4; // estimate RGBA surface
size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
chroma_log_resource_allocation("egl_surface", surface_size, "output surface");
return CHROMA_OK;
@ -663,8 +668,7 @@ void chroma_surface_destroy(chroma_output_t *output) {
}
// Log surface destruction
size_t surface_size =
(size_t)output->width * output->height * 4; // estimate RGBA surface
size_t surface_size = (size_t)output->width * (size_t)output->height * 4;
chroma_log_resource_deallocation("egl_surface", surface_size,
"output surface cleanup");
@ -749,13 +753,13 @@ int chroma_render_wallpaper(chroma_state_t *state, chroma_output_t *output) {
GLint position_attr = glGetAttribLocation(output->shader_program, "position");
GLint texcoord_attr = glGetAttribLocation(output->shader_program, "texcoord");
glEnableVertexAttribArray(position_attr);
glVertexAttribPointer(position_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *)0);
glEnableVertexAttribArray((GLuint)position_attr);
glVertexAttribPointer((GLuint)position_attr, 2, GL_FLOAT, GL_FALSE,
4 * sizeof(float), (void *)0);
glEnableVertexAttribArray(texcoord_attr);
glVertexAttribPointer(texcoord_attr, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float),
(void *)(2 * sizeof(float)));
glEnableVertexAttribArray((GLuint)texcoord_attr);
glVertexAttribPointer((GLuint)texcoord_attr, 2, GL_FLOAT, GL_FALSE,
4 * sizeof(float), (void *)(2 * sizeof(float)));
// Draw
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

View file

@ -125,7 +125,7 @@ static char *expand_env_vars(const char *str) {
break;
}
size_t var_len = end - p;
size_t var_len = (size_t)(end - p);
char *var_name = malloc(var_len + 1);
if (!var_name) {
free(result);
@ -165,7 +165,7 @@ static char *expand_env_vars(const char *str) {
result = tmp;
strcat(result, "$");
} else {
size_t var_len = p - start;
size_t var_len = (size_t)(p - start);
char *var_name = malloc(var_len + 1);
if (!var_name) {
free(result);

View file

@ -397,13 +397,14 @@ void chroma_output_remove(chroma_state_t *state, uint32_t id) {
free(output->description);
// Remove from array by shifting remaining elements
int index = output - state->outputs;
int remaining = state->output_count - index - 1;
chroma_log("TRACE", "Removing output %u from array: index=%d, remaining=%d",
ptrdiff_t index = output - state->outputs;
size_t remaining = (size_t)(state->output_count - index - 1);
chroma_log("TRACE", "Removing output %u from array: index=%td, remaining=%zu",
id, index, remaining);
if (remaining > 0) {
memmove(output, output + 1, remaining * sizeof(chroma_output_t));
chroma_log("TRACE", "Shifted %d outputs in array after removal", remaining);
chroma_log("TRACE", "Shifted %zu outputs in array after removal",
remaining);
}
state->output_count--;

253
tests/test.c Normal file
View file

@ -0,0 +1,253 @@
#include "test_common.h"
#include <stdio.h>
#include <stdlib.h>
int test_failures = 0;
int test_total = 0;
static int test_null_image_handling(void) {
uint8_t *result = downsample_image(NULL, 100, 100, 4, NULL, NULL, 0.5f);
TEST_ASSERT(result == NULL, "Null image should return NULL");
return TEST_PASSED;
}
static int test_zero_dimensions(void) {
int dw, dh;
uint8_t *result = downsample_image(NULL, 0, 0, 4, &dw, &dh, 0.5f);
TEST_ASSERT(result == NULL, "Zero dimensions should return NULL");
return TEST_PASSED;
}
static int test_scale_one_preserves_size(void) {
uint8_t *src = create_uniform_image(100, 100, 128, 64, 255);
TEST_ASSERT_PTR_NOT_NULL(src, "Source image allocation");
int dw, dh;
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 1.0f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Scale 1.0 result");
TEST_ASSERT_EQ(dw, 100, "Width preserved at scale 1.0");
TEST_ASSERT_EQ(dh, 100, "Height preserved at scale 1.0");
int match = compare_images(src, dst, 100, 100, 4, 0.01f);
TEST_ASSERT(match == 1, "Image data preserved at scale 1.0");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_downsample_half_size(void) {
uint8_t *src = create_gradient_image(100, 100);
TEST_ASSERT_PTR_NOT_NULL(src, "Gradient source");
int dw, dh;
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 0.5f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Downsample result");
TEST_ASSERT_EQ(dw, 50, "Width halved at 0.5 scale");
TEST_ASSERT_EQ(dh, 50, "Height halved at 0.5 scale");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_minimum_one_pixel(void) {
uint8_t *src = create_uniform_image(1, 1, 100, 100, 100);
TEST_ASSERT_PTR_NOT_NULL(src, "Single pixel source");
int dw, dh;
uint8_t *dst = downsample_image(src, 1, 1, 4, &dw, &dh, 0.01f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Minimum size result");
TEST_ASSERT_EQ(dw, 1, "Width minimum 1 pixel");
TEST_ASSERT_EQ(dh, 1, "Height minimum 1 pixel");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_large_image_handling(void) {
uint8_t *src = create_noise_image(4096, 4096, 42);
TEST_ASSERT_PTR_NOT_NULL(src, "Large image allocation");
int dw, dh;
uint8_t *dst = downsample_image(src, 4096, 4096, 4, &dw, &dh, 0.25f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Large image downsample");
TEST_ASSERT_EQ(dw, 1024, "Correct width at 0.25 scale");
TEST_ASSERT_EQ(dh, 1024, "Correct height at 0.25 scale");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_alpha_channel_preserved(void) {
uint8_t *src = create_uniform_image(50, 50, 255, 0, 0);
TEST_ASSERT_PTR_NOT_NULL(src, "Red source");
int dw, dh;
uint8_t *dst = downsample_image(src, 50, 50, 4, &dw, &dh, 0.5f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Downsample with alpha");
TEST_ASSERT_EQ(dw, 25, "Width halved");
TEST_ASSERT_EQ(dh, 25, "Height halved");
TEST_ASSERT(dst[0 * 4 + 3] == 255, "Alpha channel preserved");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_uniform_color_accuracy(void) {
uint8_t *src = create_uniform_image(100, 100, 200, 100, 50);
TEST_ASSERT_PTR_NOT_NULL(src, "Uniform color source");
int dw, dh;
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 0.25f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Uniform color downsample");
int correct = 1;
for (int i = 0; i < dw * dh; i++) {
if (dst[i * 4 + 0] != 200 || dst[i * 4 + 1] != 100 || dst[i * 4 + 2] != 50) {
correct = 0;
break;
}
}
TEST_ASSERT(correct == 1, "Uniform color preserved in downsampling");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_gradient_smoothness(void) {
uint8_t *src = create_gradient_image(100, 100);
TEST_ASSERT_PTR_NOT_NULL(src, "Gradient source");
int dw, dh;
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 0.5f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Gradient downsample");
float psnr = compute_psnr(src, dst, dw, dh, 4);
TEST_ASSERT_FTZ(psnr, 9.0f, 5.0f, "Gradient PSNR within expected range");
free(src);
free(dst);
return TEST_PASSED;
}
static int test_checkerboard_no_aliasing(void) {
uint8_t *src = create_checkerboard(100, 100, 10);
TEST_ASSERT_PTR_NOT_NULL(src, "Checkerboard source");
int dw, dh;
uint8_t *dst = downsample_image(src, 100, 100, 4, &dw, &dh, 0.5f);
TEST_ASSERT_PTR_NOT_NULL(dst, "Checkerboard downsample");
TEST_ASSERT_EQ(dw, 50, "Width halved");
TEST_ASSERT_EQ(dh, 50, "Height halved");
free(src);
free(dst);
return TEST_PASSED;
}
static TestCase tests[] = {
{"null_image_handling", test_null_image_handling},
{"zero_dimensions", test_zero_dimensions},
{"scale_one_preserves_size", test_scale_one_preserves_size},
{"downsample_half_size", test_downsample_half_size},
{"minimum_one_pixel", test_minimum_one_pixel},
{"large_image_handling", test_large_image_handling},
{"alpha_channel_preserved", test_alpha_channel_preserved},
{"uniform_color_accuracy", test_uniform_color_accuracy},
{"gradient_smoothness", test_gradient_smoothness},
{"checkerboard_no_aliasing", test_checkerboard_no_aliasing},
};
int main(int argc, char **argv) {
int profile = 0;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--profile") == 0) {
profile = 1;
}
}
if (profile) {
const char *scenarios[] = {"No_Downsampling", "1080p_Target", "1440p_Target", "4K_Target"};
float scales[] = {1.0f, 0.5f, 0.42f, 0.25f};
for (int s = 0; s < 4; s++) {
char filename[256];
snprintf(filename, sizeof(filename), "/tmp/chroma_memory_%s.csv", scenarios[s]);
FILE *f = fopen(filename, "w");
if (!f) {
fprintf(stderr, "Failed to create %s\n", filename);
continue;
}
fprintf(f, "Resolution,OriginalSizeMB,DownsampledSizeMB,MemorySavingsPercent\n");
const char *res_names[] = {"1080p", "1440p", "4K", "5K", "6K", "8K"};
int widths[] = {1920, 2560, 3840, 5120, 6016, 7680};
int heights[] = {1080, 1440, 2160, 2880, 3200, 4320};
for (int r = 0; r < 6; r++) {
int w = widths[r];
int h = heights[r];
size_t original_bytes = (size_t)w * (size_t)h * 4u;
double original_mb = (double)original_bytes / (1024.0 * 1024.0);
int dw, dh;
uint8_t *src = create_noise_image(w, h, 42);
uint8_t *dst = downsample_image(src, w, h, 4, &dw, &dh, scales[s]);
size_t downsampled_bytes = (size_t)dw * (size_t)dh * 4u;
double downsampled_mb = (double)downsampled_bytes / (1024.0 * 1024.0);
double savings = 0.0;
if (original_mb > 0) {
savings = ((original_mb - downsampled_mb) / original_mb) * 100.0;
}
fprintf(f, "%s,%.2f,%.2f,%.1f\n", res_names[r], original_mb, downsampled_mb, savings);
free(src);
free(dst);
}
fclose(f);
printf("Generated: %s\n", filename);
}
return 0;
}
(void)argc;
(void)argv;
printf("Chroma Unit Tests\n");
printf("=================\n\n");
test_failures = 0;
test_total = 0;
for (int i = 0; i < (int)(sizeof(tests) / sizeof(tests[0])); i++) {
int result = tests[i].fn();
test_total++;
if (result == TEST_PASSED) {
printf(" [PASS] %s\n", tests[i].name);
} else {
printf(" [FAIL] %s\n", tests[i].name);
}
}
printf("\n-----------------\n");
printf("Results: %d/%d passed\n", test_total - test_failures, test_total);
return test_failures > 0 ? 1 : 0;
}

91
tests/util/test_common.h Normal file
View file

@ -0,0 +1,91 @@
#ifndef TEST_COMMON_H
#define TEST_COMMON_H
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <math.h>
#include <time.h>
#define TEST_PASSED 0
#define TEST_FAILED 1
typedef struct {
const char *name;
int (*fn)(void);
} TestCase;
typedef struct {
const char *name;
double (*fn)(void);
} BenchCase;
extern int test_failures;
extern int test_total;
#define TEST_ASSERT(cond, msg) do { \
if (!(cond)) { \
fprintf(stderr, " [FAIL] %s\n", msg); \
test_failures++; \
return TEST_FAILED; \
} \
} while (0)
#define TEST_ASSERT_EQ(actual, expected, msg) do { \
if ((actual) != (expected)) { \
fprintf(stderr, " [FAIL] %s: expected %ld, got %ld\n", msg, (long)(expected), (long)(actual)); \
test_failures++; \
return TEST_FAILED; \
} \
} while (0)
#define TEST_ASSERT_FTZ(actual, expected, tol, msg) do { \
double _actual = (double)(actual); \
double _expected = (double)(expected); \
double _tol = (double)(tol); \
double _diff = fabs(_actual - _expected); \
if (_diff > _tol) { \
fprintf(stderr, " [FAIL] %s: expected %.6f, got %.6f (diff %.6f)\n", msg, _expected, _actual, _diff); \
test_failures++; \
return TEST_FAILED; \
} \
} while (0)
#define TEST_ASSERT_PTR_NOT_NULL(ptr, msg) do { \
if ((ptr) == NULL) { \
fprintf(stderr, " [FAIL] %s: pointer is NULL\n", msg); \
test_failures++; \
return TEST_FAILED; \
} \
} while (0)
#define RUN_TEST(tests, name) do { \
int _result = (name)(); \
test_total++; \
if (_result == TEST_PASSED) { \
printf(" [PASS] %s\n", #name); \
} \
} while (0)
#define RUN_BENCH(benchmarks, name, iter) do { \
double _time = run_benchmark((name), (iter)); \
printf(" [BENCH] %-40s %.3f ms\n", #name, _time); \
} while (0)
extern int test_failures;
extern int test_total;
uint8_t *load_image(const char *path, int *width, int *height, int *channels);
int save_image(const char *path, uint8_t *data, int width, int height, int channels);
uint8_t *downsample_image(uint8_t *src, int sw, int sh, int sc, int *dw, int *dh, float scale);
double get_time_ms(void);
double run_benchmark(double (*fn)(void), int iterations);
int compare_images(uint8_t *a, uint8_t *b, int w, int h, int ch, float threshold);
float compute_psnr(uint8_t *a, uint8_t *b, int w, int h, int ch);
uint8_t *create_gradient_image(int w, int h);
uint8_t *create_noise_image(int w, int h, unsigned int seed);
uint8_t *create_uniform_image(int w, int h, uint8_t r, uint8_t g, uint8_t b);
uint8_t *create_checkerboard(int w, int h, int check_size);
#endif