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:
commit
50c41fa883
20 changed files with 1522 additions and 146 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -12,3 +12,5 @@ test_memory
|
|||
test_config
|
||||
*.jpg
|
||||
*.conf
|
||||
vgcore.*
|
||||
*_report.txt
|
||||
|
|
|
|||
57
Makefile
57
Makefile
|
|
@ -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-%:
|
||||
|
|
|
|||
60
README.md
60
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
|
||||
|
||||
<!--markdownlint-disable MD059 -->
|
||||
|
|
|
|||
332
benchmarks/bench.c
Normal file
332
benchmarks/bench.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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
8
include/chroma.h
vendored
|
|
@ -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
190
lib/test_common.c
Normal 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;
|
||||
}
|
||||
|
|
@ -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
378
scripts/generate_report.py
Normal 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()
|
||||
|
|
@ -8,6 +8,7 @@ pkgs.mkShell {
|
|||
gdb
|
||||
valgrind
|
||||
strace
|
||||
bear
|
||||
|
||||
# Code formatting and analysis
|
||||
clang-tools # includes clang-format
|
||||
|
|
|
|||
105
src/config.c
105
src/config.c
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
10
src/core.c
10
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,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 &&
|
||||
|
|
|
|||
39
src/image.c
39
src/image.c
|
|
@ -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);
|
||||
|
|
|
|||
70
src/render.c
70
src/render.c
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
253
tests/test.c
Normal 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
91
tests/util/test_common.h
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue