Compare commits
10 commits
1891725ff9
...
c8d5637e25
| Author | SHA1 | Date | |
|---|---|---|---|
|
c8d5637e25 |
|||
|
459989e896 |
|||
|
e871307f6a |
|||
|
000258df5c |
|||
|
4a84ed7a21 |
|||
|
9be9c8276a |
|||
|
4d10074181 |
|||
|
5df01492ec |
|||
|
23527908c2 |
|||
|
3db813fcc2 |
11 changed files with 993 additions and 69 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -12,3 +12,5 @@ test_memory
|
|||
test_config
|
||||
*.jpg
|
||||
*.conf
|
||||
vgcore.*
|
||||
*_report.txt
|
||||
|
|
|
|||
34
Makefile
34
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-%:
|
||||
|
|
|
|||
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 -->
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
71
src/config.c
71
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);
|
||||
|
||||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 @ "
|
||||
|
|
|
|||
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 * 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue