/* This file is part of frei0r (https://frei0r.dyne.org)
*
* Copyright (C) 2024-2025 Dyne.org foundation
* designed, written and maintained by Denis Roio <jaromil@dyne.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
*/
#include <unistd.h>
#include <libgen.h>
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <frei0r.h>
#include "test-pattern.h"
#if defined(__linux__) && defined(GUI)
#pragma message "Compiling GUI test"
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#endif
// frei0r function prototypes
typedef int (*f0r_init_f)(void);
typedef void (*f0r_deinit_f)(void);
typedef void (*f0r_get_plugin_info_f)(f0r_plugin_info_t *info);
typedef void (*f0r_get_param_info_f)(f0r_param_info_t *info, int param_index);
typedef f0r_instance_t (*f0r_construct_f)(unsigned int width, unsigned int height);
typedef void (*f0r_update_f)(f0r_instance_t instance,
double time, const uint32_t* inframe, uint32_t* outframe);
typedef void (*f0r_update2_f)(f0r_instance_t instance, double time,
const uint32_t* inframe1, const uint32_t* inframe2,
const uint32_t* inframe3, uint32_t* outframe);
typedef void (*f0r_destruct_f)(f0r_instance_t instance);
typedef void (*f0r_set_param_value_f)(f0r_instance_t instance, f0r_param_t param, int param_index);
typedef void (*f0r_get_param_value_f)(f0r_instance_t instance, f0r_param_t param, int param_index);
// Generate a simple color bar test pattern
void generate_test_pattern(uint32_t* frame, int width, int height, int color_model, int pattern_variant) {
// Create color bars: red, green, blue, white, black, cyan, magenta, yellow
int bar_width = width / 8;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int bar_index = x / bar_width;
if (bar_index >= 8) bar_index = 7;
// Rotate pattern based on variant
if (pattern_variant == 1) {
// Vertical bars instead
bar_index = y / (height / 8);
if (bar_index >= 8) bar_index = 7;
} else if (pattern_variant == 2) {
// Checkerboard
bar_index = ((x / bar_width) + (y / (height / 8))) % 8;
}
uint8_t r, g, b, a = 255;
switch (bar_index) {
case 0: // Red
r = 255; g = 0; b = 0;
break;
case 1: // Green
r = 0; g = 255; b = 0;
break;
case 2: // Blue
r = 0; g = 0; b = 255;
break;
case 3: // White
r = 255; g = 255; b = 255;
break;
case 4: // Black
r = 0; g = 0; b = 0;
break;
case 5: // Cyan
r = 0; g = 255; b = 255;
break;
case 6: // Magenta
r = 255; g = 0; b = 255;
break;
case 7: // Yellow
r = 255; g = 255; b = 0;
break;
default:
r = 128; g = 128; b = 128;
break;
}
// Add some vertical variation for visual interest
if (y < height / 4) {
// Top quarter: keep original colors
} else if (y < height / 2) {
// Second quarter: darker
r = r * 0.7;
g = g * 0.7;
b = b * 0.7;
} else if (y < 3 * height / 4) {
// Third quarter: even darker
r = r * 0.4;
g = g * 0.4;
b = b * 0.4;
} else {
// Bottom quarter: much darker
r = r * 0.2;
g = g * 0.2;
b = b * 0.2;
}
if (color_model == F0R_COLOR_MODEL_BGRA8888) {
frame[y * width + x] = (a << 24) | (r << 16) | (g << 8) | b;
} else {
frame[y * width + x] = (a << 24) | (b << 16) | (g << 8) | r;
}
}
}
}
// Test parameters by cycling through different values
void test_parameters(f0r_instance_t instance, f0r_set_param_value_f f0r_set_param_value,
f0r_get_param_info_f f0r_get_param_info, int num_params, int frame_count) {
f0r_param_info_t param_info;
double double_val;
f0r_param_color_t color_val;
f0r_param_position_t position_val;
for (int i = 0; i < num_params; i++) {
f0r_get_param_info(¶m_info, i);
switch (param_info.type) {
case F0R_PARAM_BOOL:
// Alternate between 0.0 and 1.0 every 30 frames
double_val = (frame_count / 30) % 2;
f0r_set_param_value(instance, (f0r_param_t)&double_val, i);
break;
case F0R_PARAM_DOUBLE:
// Cycle through values 0.0 to 1.0 over 60 frames
double_val = (frame_count % 60) / 60.0;
f0r_set_param_value(instance, (f0r_param_t)&double_val, i);
break;
case F0R_PARAM_COLOR:
// Cycle through different colors
switch ((frame_count / 20) % 4) {
case 0: // Red
color_val.r = 1.0; color_val.g = 0.0; color_val.b = 0.0;
break;
case 1: // Green
color_val.r = 0.0; color_val.g = 1.0; color_val.b = 0.0;
break;
case 2: // Blue
color_val.r = 0.0; color_val.g = 0.0; color_val.b = 1.0;
break;
case 3: // White
color_val.r = 1.0; color_val.g = 1.0; color_val.b = 1.0;
break;
}
f0r_set_param_value(instance, (f0r_param_t)&color_val, i);
break;
case F0R_PARAM_POSITION:
// Move position in a circular pattern
position_val.x = 0.5 + 0.4 * sin(frame_count * 0.1);
position_val.y = 0.5 + 0.4 * cos(frame_count * 0.1);
f0r_set_param_value(instance, (f0r_param_t)&position_val, i);
break;
case F0R_PARAM_STRING:
{
// For string parameters, use "0" as default
char *string_val = "0";
f0r_set_param_value(instance, (f0r_param_t)&string_val, i);
break;
}
default:
// For unknown parameter types, set to middle value
double_val = 0.5;
f0r_set_param_value(instance, (f0r_param_t)&double_val, i);
break;
}
}
}
int main(int argc, char* argv[]) {
// instance frei0r pointers
static void *dl_handle;
static f0r_init_f f0r_init;
static f0r_deinit_f f0r_deinit;
static f0r_plugin_info_t pi;
static f0r_get_plugin_info_f f0r_get_plugin_info;
static f0r_get_param_info_f f0r_get_param_info;
static f0r_param_info_t param;
static f0r_instance_t instance;
static f0r_construct_f f0r_construct;
static f0r_update_f f0r_update;
static f0r_destruct_f f0r_destruct;
static f0r_set_param_value_f f0r_set_param_value;
static f0r_get_param_value_f f0r_get_param_value;
const char *usage = "Usage: frei0r-run [-tdg] [-f frames] -p <frei0r_plugin_file>\n"
" -d debug mode\n"
" -g graphical display mode (Linux/WSL)\n"
" -f frames number of frames to process (default: 100)\n"
" -p plugin path to frei0r plugin file";
if (argc < 2) {
fprintf(stderr,"%s\n",usage);
return -1;
}
int opt;
#if defined(GUI)
int graphical = 1;
#else
int graphical = 0;
#endif
int debug = 0;
int frames = 100; // Number of frames to test
char plugin_file[512];
plugin_file[0] = '\0';
while((opt = getopt(argc, argv, "tdgf:p:")) != -1) {
switch(opt) {
case 'd':
debug = 1;
break;
case 'g':
graphical = 1;
break;
case 'f':
frames = atoi(optarg);
break;
case 'p':
snprintf(plugin_file, 511, "%s", optarg);
break;
}
}
if (plugin_file[0] == '\0') {
fprintf(stderr, "Error: plugin file required (-p option)\n%s\n", usage);
return -1;
}
// Set fixed video properties for test pattern
int frame_width = 640;
int frame_height = 480;
int fps = 30;
const char *file = basename(plugin_file);
const char *dir = dirname(plugin_file);
char path[256];;
snprintf(path, 255,"%s/%s",dir,file);
// fprintf(stderr,"%s %s\n",argv[0], file);
// load shared library
dl_handle = dlopen(path, RTLD_NOW|RTLD_LOCAL|RTLD_NODELETE);
if(!dl_handle) {
fprintf(stderr,"error: %s\n",dlerror());
exit(1);
}
// get plugin function calls
f0r_init = (f0r_init_f) dlsym(dl_handle,"f0r_init");
f0r_deinit = (f0r_deinit_f) dlsym(dl_handle,"f0r_deinit");
f0r_get_plugin_info = (f0r_get_plugin_info_f) dlsym(dl_handle,"f0r_get_plugin_info");
f0r_get_param_info = (f0r_get_param_info_f) dlsym(dl_handle,"f0r_get_param_info");
f0r_construct = (f0r_construct_f) dlsym(dl_handle,"f0r_construct");
f0r_update = (f0r_update_f) dlsym(dl_handle,"f0r_update");
f0r_destruct = (f0r_destruct_f) dlsym(dl_handle,"f0r_destruct");
f0r_set_param_value = (f0r_set_param_value_f) dlsym(dl_handle,"f0r_set_param_value");
f0r_get_param_value = (f0r_get_param_value_f) dlsym(dl_handle,"f0r_get_param_value");
// always initialize plugin first
f0r_init();
// get info about plugin
f0r_get_plugin_info(&pi);
const char *frei0r_color_model = (pi.color_model == F0R_COLOR_MODEL_BGRA8888 ? "bgra8888" :
pi.color_model == F0R_COLOR_MODEL_RGBA8888 ? "rgba8888" :
pi.color_model == F0R_COLOR_MODEL_PACKED32 ? "packed32" : "unknown");
if(debug) {
fprintf(stderr,"{\n \"name\":\"%s\",\n \"type\":\"%s\",\n \"color_model\":\"%s\",\n \"num_params\":\"%d\"\n}",
pi.name,
pi.plugin_type == F0R_PLUGIN_TYPE_FILTER ? "filter" :
pi.plugin_type == F0R_PLUGIN_TYPE_SOURCE ? "source" :
pi.plugin_type == F0R_PLUGIN_TYPE_MIXER2 ? "mixer2" :
pi.plugin_type == F0R_PLUGIN_TYPE_MIXER3 ? "mixer3" : "unknown",
frei0r_color_model,
pi.num_params);
// Print parameter information
if (pi.num_params > 0) {
fprintf(stderr,",\n \"parameters\":[\n");
for (int i = 0; i < pi.num_params; i++) {
f0r_get_param_info(¶m, i);
const char* param_type =
param.type == F0R_PARAM_BOOL ? "bool" :
param.type == F0R_PARAM_DOUBLE ? "double" :
param.type == F0R_PARAM_COLOR ? "color" :
param.type == F0R_PARAM_POSITION ? "position" :
param.type == F0R_PARAM_STRING ? "string" : "unknown";
fprintf(stderr," {\"name\":\"%s\",\"type\":\"%s\",\"explanation\":\"%s\"}",
param.name, param_type, param.explanation);
if (i < pi.num_params - 1) fprintf(stderr,",\n");
}
fprintf(stderr,"\n ]\n");
}
fprintf(stderr,"}\n");
}
instance = f0r_construct(frame_width, frame_height);
uint32_t *input_buffer = NULL;
uint32_t *input_buffer2 = NULL;
uint32_t *input_buffer3 = NULL;
uint32_t *output_buffer;
// Allocate buffers based on plugin type
if (pi.plugin_type == F0R_PLUGIN_TYPE_FILTER) {
input_buffer = (uint32_t*)calloc(4, frame_width * frame_height);
} else if (pi.plugin_type == F0R_PLUGIN_TYPE_MIXER2) {
input_buffer = (uint32_t*)calloc(4, frame_width * frame_height);
input_buffer2 = (uint32_t*)calloc(4, frame_width * frame_height);
} else if (pi.plugin_type == F0R_PLUGIN_TYPE_MIXER3) {
input_buffer = (uint32_t*)calloc(4, frame_width * frame_height);
input_buffer2 = (uint32_t*)calloc(4, frame_width * frame_height);
input_buffer3 = (uint32_t*)calloc(4, frame_width * frame_height);
}
// SOURCE type needs no input buffer
output_buffer = (uint32_t*)calloc(4, frame_width * frame_height);
#if defined(GUI)
// Generate initial test patterns
if (input_buffer)
generate_animated_test_pattern(input_buffer, frame_width, frame_height, 0, pi.color_model);
if (input_buffer2)
generate_animated_test_pattern(input_buffer2, frame_width, frame_height, 0, pi.color_model);
if (input_buffer3)
generate_animated_test_pattern(input_buffer3, frame_width, frame_height, 0, pi.color_model);
#else
if (input_buffer)
generate_test_pattern(input_buffer, frame_width, frame_height, pi.color_model, 0);
if (input_buffer2)
generate_test_pattern(input_buffer2, frame_width, frame_height, pi.color_model, 1);
if (input_buffer3)
generate_test_pattern(input_buffer3, frame_width, frame_height, pi.color_model, 2);
#endif
#if defined(__linux__) && defined(GUI)
Display *display = NULL;
Window window;
GC gc;
XImage *ximage = NULL;
if (graphical) {
display = XOpenDisplay(NULL);
if (!display) {
fprintf(stderr, "Warning: Cannot open X display, falling back to headless mode\n");
graphical = 0;
} else {
int screen = DefaultScreen(display);
window = XCreateSimpleWindow(display, RootWindow(display, screen),
0, 0, frame_width, frame_height, 1,
BlackPixel(display, screen),
WhitePixel(display, screen));
XStoreName(display, window, pi.name);
XSelectInput(display, window, ExposureMask | KeyPressMask);
XMapWindow(display, window);
gc = XCreateGC(display, window, 0, NULL);
Visual *visual = DefaultVisual(display, screen);
ximage = XCreateImage(display, visual, 24, ZPixmap, 0,
(char*)output_buffer, frame_width, frame_height, 32, 0);
XFlush(display);
}
}
#endif
// Load f0r_update2 for mixers
f0r_update2_f f0r_update2 = NULL;
if (pi.plugin_type == F0R_PLUGIN_TYPE_MIXER2 || pi.plugin_type == F0R_PLUGIN_TYPE_MIXER3) {
f0r_update2 = (f0r_update2_f)dlsym(dl_handle, "f0r_update2");
if (!f0r_update2) {
fprintf(stderr, "Error: Cannot load f0r_update2 for mixer plugin\n");
if (input_buffer) free(input_buffer);
if (input_buffer2) free(input_buffer2);
if (input_buffer3) free(input_buffer3);
free(output_buffer);
f0r_destruct(instance);
f0r_deinit();
dlclose(dl_handle);
return 1;
}
}
// Test the plugin with different parameter values
for (int frame = 0; frame < frames; frame++) {
#if defined(GUI)
// Generate animated test patterns for this frame
if (input_buffer)
generate_animated_test_pattern(input_buffer, frame_width, frame_height, frame, pi.color_model);
if (input_buffer2)
generate_animated_test_pattern(input_buffer2, frame_width, frame_height, frame + 10, pi.color_model);
if (input_buffer3)
generate_animated_test_pattern(input_buffer3, frame_width, frame_height, frame + 20, pi.color_model);
#else
if (input_buffer)
generate_test_pattern(input_buffer, frame_width, frame_height, pi.color_model, 0);
if (input_buffer2)
generate_test_pattern(input_buffer2, frame_width, frame_height, pi.color_model, 1);
if (input_buffer3)
generate_test_pattern(input_buffer3, frame_width, frame_height, pi.color_model, 2);
#endif
// Update parameters if the plugin has any
if (pi.num_params > 0 && f0r_set_param_value) {
test_parameters(instance, f0r_set_param_value, f0r_get_param_info, pi.num_params, frame);
}
// Apply plugin based on type
double time = (double)frame / (double)fps;
switch (pi.plugin_type) {
case F0R_PLUGIN_TYPE_SOURCE:
f0r_update(instance, time, NULL, output_buffer);
break;
case F0R_PLUGIN_TYPE_FILTER:
f0r_update(instance, time, (const uint32_t*)input_buffer, output_buffer);
break;
case F0R_PLUGIN_TYPE_MIXER2:
f0r_update2(instance, time, (const uint32_t*)input_buffer,
(const uint32_t*)input_buffer2, NULL, output_buffer);
break;
case F0R_PLUGIN_TYPE_MIXER3:
f0r_update2(instance, time, (const uint32_t*)input_buffer,
(const uint32_t*)input_buffer2, (const uint32_t*)input_buffer3,
output_buffer);
break;
default:
fprintf(stderr, "Unknown plugin type: %d\n", pi.plugin_type);
break;
}
#if defined(__linux__) && defined(GUI)
if (graphical && display) {
ximage->data = (char*)output_buffer;
XPutImage(display, window, gc, ximage, 0, 0, 0, 0, frame_width, frame_height);
XFlush(display);
// Check for key press to exit early
while (XPending(display)) {
XEvent event;
XNextEvent(display, &event);
if (event.type == KeyPress) {
fprintf(stderr, "\nInterrupted by user at frame %d\n", frame);
frame = frames; // Exit loop
break;
}
}
usleep(1000000 / fps); // Frame delay
}
#endif
if (!graphical && frame % 10 == 0 && debug) {
printf("Frame %d processed\n", frame);
}
}
#if defined(__linux__) && defined(GUI)
if (graphical && display) {
ximage->data = NULL; // Prevent XDestroyImage from freeing our buffer
XDestroyImage(ximage);
XFreeGC(display, gc);
XDestroyWindow(display, window);
XCloseDisplay(display);
}
#endif
if(debug) {
const char *plugin_type_name = "unknown";
switch (pi.plugin_type) {
case F0R_PLUGIN_TYPE_SOURCE: plugin_type_name = "source"; break;
case F0R_PLUGIN_TYPE_FILTER: plugin_type_name = "filter"; break;
case F0R_PLUGIN_TYPE_MIXER2: plugin_type_name = "mixer2"; break;
case F0R_PLUGIN_TYPE_MIXER3: plugin_type_name = "mixer3"; break;
}
printf("Test completed successfully. Plugin: %s (type: %s)\n", pi.name, plugin_type_name);
printf("Tested %d frames with %d parameters\n", frames, pi.num_params);
if (pi.plugin_type != F0R_PLUGIN_TYPE_SOURCE) {
printf("Input: %dx%d test pattern(s)\n", frame_width, frame_height);
}
printf("Output: %dx%d processed frames\n", frame_width, frame_height);
}
if (input_buffer) free(input_buffer);
if (input_buffer2) free(input_buffer2);
if (input_buffer3) free(input_buffer3);
free(output_buffer);
f0r_destruct(instance);
f0r_deinit();
dlclose(dl_handle);
return 0;
}