From e177e32bfd8cd3763d6a625011ee8cd2dfc9c815 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 17 Jan 2026 19:09:40 +0300 Subject: [PATCH 01/16] nix: add bear to devshell Signed-off-by: NotAShelf Change-Id: Ib56e2256b279f69fccb00f32423a7d0d6a6a6964 --- shell.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/shell.nix b/shell.nix index e6e3354..ba4e522 100644 --- a/shell.nix +++ b/shell.nix @@ -8,6 +8,7 @@ pkgs.mkShell { gdb valgrind strace + bear # Code formatting and analysis clang-tools # includes clang-format -- 2.43.0 From 1891725ff9cd7fc3a935a20216084b9717c399e1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:16:17 +0300 Subject: [PATCH 02/16] tests: add basic helpers Signed-off-by: NotAShelf Change-Id: I6e5659a4a93d62c6ae60dd1f1a03425a6a6a6964 --- tests/util/test_common.h | 88 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/util/test_common.h diff --git a/tests/util/test_common.h b/tests/util/test_common.h new file mode 100644 index 0000000..56d5bab --- /dev/null +++ b/tests/util/test_common.h @@ -0,0 +1,88 @@ +#ifndef TEST_COMMON_H +#define TEST_COMMON_H + +#include +#include +#include +#include +#include +#include + +#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 _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 -- 2.43.0 From 3db813fcc2349a6440e1ab958cbd3ab67961dcb1 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:17:13 +0300 Subject: [PATCH 03/16] tests: initial unit testing Signed-off-by: NotAShelf Change-Id: Ib67a52ddcdbb9d5378dc3dd2dd7b5d106a6a6964 --- tests/test.c | 253 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 253 insertions(+) create mode 100644 tests/test.c diff --git a/tests/test.c b/tests/test.c new file mode 100644 index 0000000..0ee79e5 --- /dev/null +++ b/tests/test.c @@ -0,0 +1,253 @@ +#include "test_common.h" +#include +#include + +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 * h * 4; + double original_mb = 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 * dh * 4; + double downsampled_mb = 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; +} -- 2.43.0 From 23527908c28dc5cc0890411240419c1f5183db35 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:17:39 +0300 Subject: [PATCH 04/16] config: fix wording Signed-off-by: NotAShelf Change-Id: I41e3e89470fa8181d887de0584c965176a6a6964 --- src/config.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/config.c b/src/config.c index 9bae27c..3f713d3 100644 --- a/src/config.c +++ b/src/config.c @@ -196,7 +196,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); @@ -419,9 +419,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) -- 2.43.0 From 5df01492ec6fbbbabb58816ea19e12f00905b888 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:17:51 +0300 Subject: [PATCH 05/16] docs: simplify README Signed-off-by: NotAShelf Change-Id: I893d51c6a084a0ed56a27cf0bcfae14b6a6a6964 --- README.md | 60 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 68cdf6d..7018606 100644 --- a/README.md +++ b/README.md @@ -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 -- 2.43.0 From 4d100741815950891775083d36a2f08d8da3b500 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:22:09 +0300 Subject: [PATCH 06/16] nix: streamline packaging Signed-off-by: NotAShelf Change-Id: I4643ed2c8e6f8ceb5e722612cc67a74e6a6a6964 --- nix/package.nix | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/nix/package.nix b/nix/package.nix index 10d2387..497f106 100644 --- a/nix/package.nix +++ b/nix/package.nix @@ -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 ''; -- 2.43.0 From 9be9c8276a56161d5d4be43bf349e901faa5e5f3 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:22:24 +0300 Subject: [PATCH 07/16] lib: add test helpers Signed-off-by: NotAShelf Change-Id: Iaefc0d503288b4ffe8e6922130acc2ec6a6a6964 --- lib/test_common.c | 190 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 190 insertions(+) create mode 100644 lib/test_common.c diff --git a/lib/test_common.c b/lib/test_common.c new file mode 100644 index 0000000..c19e34f --- /dev/null +++ b/lib/test_common.c @@ -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 +#include + +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; +} -- 2.43.0 From 4a84ed7a212298f4a3d00773a633bf6d55357ae6 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:22:35 +0300 Subject: [PATCH 08/16] scripts: visualise benchmark results via Python script Signed-off-by: NotAShelf Change-Id: If48e0a1c4b265946c009b3abd9a249a96a6a6964 --- scripts/generate_report.py | 378 +++++++++++++++++++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 scripts/generate_report.py diff --git a/scripts/generate_report.py b/scripts/generate_report.py new file mode 100644 index 0000000..d089214 --- /dev/null +++ b/scripts/generate_report.py @@ -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() -- 2.43.0 From 000258df5c7d33095135ef826ae619307619d822 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:23:05 +0300 Subject: [PATCH 09/16] chore: ignore valgrind artifacts Signed-off-by: NotAShelf Change-Id: I309e751b96e858f231e224d3b345670e6a6a6964 --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index aca924b..cb8e347 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ test_memory test_config *.jpg *.conf +vgcore.* +*_report.txt -- 2.43.0 From e871307f6a9536ce9d7e95fe8157937ce4908c9a Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:23:19 +0300 Subject: [PATCH 10/16] chore: add more Make tasks for tests & benchmarks Signed-off-by: NotAShelf Change-Id: I5cabfcf1815588ffec6c8b865cd163176a6a6964 --- Makefile | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 0efc0bd..f26050d 100644 --- a/Makefile +++ b/Makefile @@ -131,11 +131,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" | xargs clang-format -i # Static analysis (requires cppcheck) analyze: @@ -147,10 +148,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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + @./bin/test + +# Run benchmarks +bench: + @echo "Running performance benchmarks..." + @$(CC) -o bin/bench benchmarks/bench.c lib/test_common.c \ + -I./include -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + @./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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + @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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + @./bin/test --profile + @echo "CSV files generated in /tmp/" # Version management targets bump-patch: @@ -213,7 +237,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-%: -- 2.43.0 From 459989e896d7e6efdfad226a36551747855a7ac8 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Sat, 31 Jan 2026 15:23:50 +0300 Subject: [PATCH 11/16] config: add description-based output matching with `desc:` prefix Signed-off-by: NotAShelf Change-Id: Idbe0661f0c491512d61ace4337ebe8cd6a6a6964 --- include/chroma.h | 8 +++--- src/config.c | 63 +++++++++++++++++++++++++++++++++++++++--------- src/core.c | 7 +++--- 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/include/chroma.h b/include/chroma.h index 6d51f92..2402d45 100644 --- a/include/chroma.h +++ b/include/chroma.h @@ -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); diff --git a/src/config.c b/src/config.c index 3f713d3..ca47487 100644 --- a/src/config.c +++ b/src/config.c @@ -3,6 +3,7 @@ #include #include #include +#include #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:" + 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] == '"') || @@ -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,25 +725,28 @@ 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); return CHROMA_OK; diff --git a/src/core.c b/src/core.c index b7a5029..8e6f3a3 100644 --- a/src/core.c +++ b/src/core.c @@ -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,8 +122,8 @@ 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 @ " -- 2.43.0 From c8d5637e25a7c155241125b1172c7a44a95a756b Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 16 Apr 2026 16:02:29 +0300 Subject: [PATCH 12/16] meta: update sample config for prefix syntax & output maching Signed-off-by: NotAShelf Change-Id: I4ef49c171e260fe8e1ea114a58af77166a6a6964 --- chroma.conf.sample | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/chroma.conf.sample b/chroma.conf.sample index d2b2c47..6afd464 100644 --- a/chroma.conf.sample +++ b/chroma.conf.sample @@ -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 -- 2.43.0 From 237a013e0331cc6f497559079d17f4659ceecd8e Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 16 Apr 2026 16:02:43 +0300 Subject: [PATCH 13/16] build: harden default flags Signed-off-by: NotAShelf Change-Id: I9a86a035c2e35a8ccbac9c7672d82dcb6a6a6964 --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index f26050d..9b83593 100644 --- a/Makefile +++ b/Makefile @@ -17,8 +17,10 @@ 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 -Wshadow -Wstrict-prototypes +CFLAGS += -Wformat=2 -Wnormalized=nfc +CFLAGS += -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)\" # Debug build flags DEBUG_CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -Og -g3 -DDEBUG -- 2.43.0 From b311a0a9693975c3f8ad2fd59d68e924021e667d Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 16 Apr 2026 20:29:12 +0300 Subject: [PATCH 14/16] build: add `-Wconversion` and `-Wdouble-promotion` to default flags Signed-off-by: NotAShelf Change-Id: I92e454f97d628a7b8ada8dc85ec458376a6a6964 --- Makefile | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 9b83593..2041d3e 100644 --- a/Makefile +++ b/Makefile @@ -18,10 +18,13 @@ SYSTEMD_INSTALL = $(HOME)/.config/systemd/user CC = gcc CFLAGS = -std=c11 -Wall -Wextra -Werror -pedantic -O2 -g CFLAGS += -fstack-protector-strong -fstack-clash-protection -CFLAGS += -fno-common -Wshadow -Wstrict-prototypes -CFLAGS += -Wformat=2 -Wnormalized=nfc +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 DEBUG_CFLAGS += -D_GNU_SOURCE -DCHROMA_VERSION=\"$(VERSION)-debug\" @@ -46,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) @@ -78,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) @@ -138,7 +153,7 @@ clean: # Format source code (requires clang-format) format: @echo "Formatting source code..." - @find $(SRCDIR) -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: @@ -153,28 +168,28 @@ analyze: test: $(TARGET) @echo "Running unit tests..." @$(CC) -o bin/test tests/test.c lib/test_common.c \ - -I./include -I./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + -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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + -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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + -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./tests -I./tests/util -lm -std=c11 -D_GNU_SOURCE $(CFLAGS) + -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/" -- 2.43.0 From 3719dbccd5b0982506636216e850fe34c764a9bb Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 16 Apr 2026 21:04:05 +0300 Subject: [PATCH 15/16] treewide: fix various build warnings; ignore vendored headers in formatting job Signed-off-by: NotAShelf Change-Id: I7af033c8d3f437e5574b050223cbc16a6a6a6964 --- include/{ => vendor}/stb_image.h | 0 include/{ => vendor}/stb_image_write.h | 0 src/config.c | 34 +++++++------ src/core.c | 3 +- src/image.c | 39 +++++++------- src/render.c | 70 ++++++++++++++------------ src/utils.c | 4 +- src/wayland.c | 9 ++-- tests/test.c | 8 +-- tests/util/test_common.h | 9 ++-- 10 files changed, 95 insertions(+), 81 deletions(-) rename include/{ => vendor}/stb_image.h (100%) rename include/{ => vendor}/stb_image_write.h (100%) diff --git a/include/stb_image.h b/include/vendor/stb_image.h similarity index 100% rename from include/stb_image.h rename to include/vendor/stb_image.h diff --git a/include/stb_image_write.h b/include/vendor/stb_image_write.h similarity index 100% rename from include/stb_image_write.h rename to include/vendor/stb_image_write.h diff --git a/src/config.c b/src/config.c index ca47487..0c28132 100644 --- a/src/config.c +++ b/src/config.c @@ -262,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; @@ -403,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; @@ -417,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); @@ -436,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); } @@ -527,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)", @@ -660,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"); @@ -677,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"); @@ -748,7 +748,8 @@ int chroma_config_get_mapping_for_output( 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; } } @@ -764,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; } @@ -786,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); diff --git a/src/core.c b/src/core.c index 8e6f3a3..6751853 100644 --- a/src/core.c +++ b/src/core.c @@ -129,7 +129,8 @@ static int assign_wallpaper_to_output(chroma_state_t *state, "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 && diff --git a/src/image.c b/src/image.c index 5b66862..96482c1 100644 --- a/src/image.c +++ b/src/image.c @@ -1,5 +1,5 @@ #define STB_IMAGE_IMPLEMENTATION -#include "../include/stb_image.h" +#include "../include/vendor/stb_image.h" #include #include @@ -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); diff --git a/src/render.c b/src/render.c index 13444f3..2ade4c4 100644 --- a/src/render.c +++ b/src/render.c @@ -6,7 +6,7 @@ #include #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); diff --git a/src/utils.c b/src/utils.c index 24b9241..72468b4 100644 --- a/src/utils.c +++ b/src/utils.c @@ -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); diff --git a/src/wayland.c b/src/wayland.c index 508ec48..e931511 100644 --- a/src/wayland.c +++ b/src/wayland.c @@ -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--; diff --git a/tests/test.c b/tests/test.c index 0ee79e5..9ce3755 100644 --- a/tests/test.c +++ b/tests/test.c @@ -201,14 +201,14 @@ int main(int argc, char **argv) { for (int r = 0; r < 6; r++) { int w = widths[r]; int h = heights[r]; - size_t original_bytes = (size_t)w * h * 4; - double original_mb = original_bytes / (1024.0 * 1024.0); + 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 * dh * 4; - double downsampled_mb = downsampled_bytes / (1024.0 * 1024.0); + 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) { diff --git a/tests/util/test_common.h b/tests/util/test_common.h index 56d5bab..7bc72a5 100644 --- a/tests/util/test_common.h +++ b/tests/util/test_common.h @@ -41,9 +41,12 @@ extern int test_total; } while (0) #define TEST_ASSERT_FTZ(actual, expected, tol, msg) do { \ - double _diff = fabs((actual) - (expected)); \ - if (_diff > (tol)) { \ - fprintf(stderr, " [FAIL] %s: expected %.6f, got %.6f (diff %.6f)\n", msg, (expected), (actual), _diff); \ + 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; \ } \ -- 2.43.0 From c3d96b1a494c3de7def6424732487ed673c7ca72 Mon Sep 17 00:00:00 2001 From: NotAShelf Date: Thu, 16 Apr 2026 21:04:32 +0300 Subject: [PATCH 16/16] build: initial benchmarking framework Signed-off-by: NotAShelf Change-Id: I67e686185114daa167e5de589e53f53f6a6a6964 --- benchmarks/bench.c | 332 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 benchmarks/bench.c diff --git a/benchmarks/bench.c b/benchmarks/bench.c new file mode 100644 index 0000000..6872fef --- /dev/null +++ b/benchmarks/bench.c @@ -0,0 +1,332 @@ +#include "test_common.h" +#include +#include +#include +#include + +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; +} -- 2.43.0