diff --git a/.github/scripts/detect-new-plugins.sh b/.github/scripts/detect-new-plugins.sh new file mode 100755 index 0000000..e66b157 --- /dev/null +++ b/.github/scripts/detect-new-plugins.sh @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +set -euo pipefail + +base_sha=${1:?base sha required} +head_sha=${2:?head sha required} + +mode=full +reason="changed files require the full suite" +declare -a plugin_dirs=() +declare -a targets=() +declare -a plugin_paths=() + +fatal() { + printf 'error: %s\n' "$1" >&2 + exit 1 +} + +is_plugin_tree() { + case "$1" in + src/filter/*|src/mixer2/*|src/mixer3/*) return 0 ;; + *) return 1 ;; + esac +} + +plugin_dir_from_path() { + local path=$1 + IFS=/ read -r top category name _ <<< "$path" + if [[ "$top" == "src" && -n "${category:-}" && -n "${name:-}" ]]; then + printf '%s/%s/%s\n' "$top" "$category" "$name" + return 0 + fi + return 1 +} + +contains_dir() { + local needle=$1 + shift || true + local item + for item in "$@"; do + if [[ "$item" == "$needle" ]]; then + return 0 + fi + done + return 1 +} + +dir_exists_in_commit() { + local commit=$1 + local path=$2 + git cat-file -e "${commit}:${path}" 2>/dev/null +} + +parent_cmake_registers_plugin() { + local commit=$1 + local plugin_dir=$2 + local category=${plugin_dir#src/} + category=${category%%/*} + local plugin_name=${plugin_dir##*/} + local parent_cmake="src/${category}/CMakeLists.txt" + + local parent_text + parent_text=$(git show "${commit}:${parent_cmake}" 2>/dev/null) || { + return 1 + } + + printf '%s\n' "$parent_text" | grep -Eq "^[[:space:]]*add_subdirectory[[:space:]]*\\([[:space:]]*${plugin_name}[[:space:]]*\\)" +} + +extract_target_name() { + local commit=$1 + local plugin_dir=$2 + local plugin_name=${plugin_dir##*/} + local cmake_file="${plugin_dir}/CMakeLists.txt" + + local cmake_text + cmake_text=$(git show "${commit}:${cmake_file}" 2>/dev/null) || { + return 1 + } + + local target + target=$(printf '%s\n' "$cmake_text" | sed -nE 's/^[[:space:]]*set[[:space:]]*\([[:space:]]*TARGET[[:space:]]+([^[:space:])]+).*/\1/p' | head -n1) + if [[ -n "$target" ]]; then + printf '%s\n' "$target" + return 0 + fi + + target=$(printf '%s\n' "$cmake_text" | sed -nE 's/^[[:space:]]*add_library[[:space:]]*\(([[:alnum:]_.+-]+)[[:space:]]+MODULE.*/\1/p' | head -n1) + if [[ -n "$target" ]]; then + printf '%s\n' "$target" + return 0 + fi + + printf '%s\n' "$plugin_name" +} + +while IFS=$'\t' read -r status path new_path; do + [[ -n "${status:-}" ]] || continue + + if [[ "$status" == A* ]] && is_plugin_tree "$path"; then + plugin_dir=$(plugin_dir_from_path "$path") || true + if [[ -n "${plugin_dir:-}" ]] && dir_exists_in_commit "$base_sha" "$plugin_dir"; then + reason="PR adds files inside existing plugin directory: $plugin_dir" + plugin_dirs=() + break + fi + + if [[ -n "${plugin_dir:-}" ]] && ! contains_dir "$plugin_dir" "${plugin_dirs[@]}"; then + plugin_dirs+=("$plugin_dir") + fi + continue + fi + + case "$path" in + src/filter/CMakeLists.txt|src/mixer2/CMakeLists.txt|src/mixer3/CMakeLists.txt) + continue + ;; + *) + reason="PR touches non-new-plugin files: $path" + plugin_dirs=() + break + ;; + esac +done < <(git diff --name-status --find-renames "$base_sha" "$head_sha") + +if [[ ${#plugin_dirs[@]} -gt 0 ]]; then + mode=targeted + reason="PR only adds new plugin directories" + + for plugin_dir in "${plugin_dirs[@]}"; do + if ! parent_cmake_registers_plugin "$head_sha" "$plugin_dir"; then + fatal "parent CMakeLists.txt does not register ${plugin_dir}. Add add_subdirectory(${plugin_dir##*/}) to the category CMakeLists.txt." + fi + + target=$(extract_target_name "$head_sha" "$plugin_dir") || { + mode=full + reason="could not resolve target for $plugin_dir" + targets=() + plugin_paths=() + break + } + + plugin_paths+=("build/${plugin_dir}/${target}.so") + targets+=("$target") + done +fi + +printf 'mode=%s\n' "$mode" +printf 'reason=%s\n' "$reason" + +if [[ "$mode" == "targeted" ]]; then + { + printf 'targets<> $GITHUB_OUTPUT + npx semantic-release --dry-run | tee semantic-release.log + version=$(awk '/The next release version is/ { print $NF }' semantic-release.log | tail -n1) + if [[ -z "$version" ]]; then + echo "release=False" >> "$GITHUB_OUTPUT" else - echo "release=True" >> $GITHUB_OUTPUT - awk '/Published release/ { printf("version=v%s\n",$8) }' semantic-release.log >> $GITHUB_OUTPUT + echo "release=True" >> "$GITHUB_OUTPUT" + printf 'version=%s\ntag=v%s\n' "$version" "$version" >> "$GITHUB_OUTPUT" fi linux-build: name: 🐧 linux build runs-on: ubuntu-latest - needs: [semantic-release] - if: ${{ needs.semantic-release.outputs.release == 'True' }} + needs: [release-plan] + if: ${{ needs.release-plan.outputs.release == 'True' }} steps: - uses: actions/checkout@v6 - name: apt install deps run: | sudo apt-get update -y -q - sudo apt-get install -y -q --no-install-recommends cmake ninja-build libopencv-dev libgavl-dev libfreetype-dev libcairo-dev + sudo apt-get install -y -q --no-install-recommends cmake ninja-build libopencv-dev libgavl-dev libfreetype-dev libcairo2-dev - name: Build using cmake+ninja run: | mkdir build && cd build - cmake -G "Ninja" ../ + cmake -G "Ninja" -D FREI0R_VERSION=${{ needs.release-plan.outputs.version }} ../ ninja - name: Upload linux filter uses: actions/upload-artifact@v6 @@ -79,12 +81,17 @@ with: name: release-linux-generator path: build/src/generator/**/*.so + - name: Upload linux pkg-config + uses: actions/upload-artifact@v6 + with: + name: release-linux-pkgconfig + path: build/frei0r.pc win-build: name: 🪟 win64 build runs-on: windows-latest - needs: [semantic-release] - if: ${{ needs.semantic-release.outputs.release == 'True' }} + needs: [release-plan] + if: ${{ needs.release-plan.outputs.release == 'True' }} steps: - uses: actions/checkout@v6 - uses: ilammy/msvc-dev-cmd@v1 @@ -95,7 +102,7 @@ - name: Build using nmake run: | mkdir build && cd build - cmake -G "NMake Makefiles" -D WITHOUT_OPENCV=1 -D WITHOUT_CAIRO=1 -D WITHOUT_GAVL=1 ../ + cmake -G "NMake Makefiles" -D WITHOUT_OPENCV=1 -D WITHOUT_CAIRO=1 -D WITHOUT_GAVL=1 -D FREI0R_VERSION=${{ needs.release-plan.outputs.version }} ../ nmake - name: Upload win64 filter uses: actions/upload-artifact@v6 @@ -117,12 +124,17 @@ with: name: release-win64-generator path: build/src/generator/**/*.dll + - name: Upload win64 pkg-config + uses: actions/upload-artifact@v6 + with: + name: release-win64-pkgconfig + path: build/frei0r.pc osx-build: name: 🍏 osx build runs-on: macos-latest - needs: [semantic-release] - if: ${{ needs.semantic-release.outputs.release == 'True' }} + needs: [release-plan] + if: ${{ needs.release-plan.outputs.release == 'True' }} steps: - uses: actions/checkout@v6 - name: Update Homebrew @@ -135,7 +147,7 @@ - name: Build using ninja run: | mkdir build && cd build - cmake -G "Ninja" -D WITHOUT_OPENCV=1 -D WITHOUT_GAVL=1 ../ + cmake -G "Ninja" -D WITHOUT_OPENCV=1 -D WITHOUT_GAVL=1 -D FREI0R_VERSION=${{ needs.release-plan.outputs.version }} ../ ninja - name: Upload osx filter uses: actions/upload-artifact@v6 @@ -157,11 +169,43 @@ with: name: release-osx-generator path: build/src/generator/**/*.so + - name: Upload osx pkg-config + uses: actions/upload-artifact@v6 + with: + name: release-osx-pkgconfig + path: build/frei0r.pc + + publish-release: + name: 🚀 Publish release + runs-on: ubuntu-latest + needs: [release-plan, win-build, osx-build, linux-build] + if: ${{ needs.release-plan.outputs.release == 'True' }} + outputs: + version: ${{ steps.publish_release.outputs.version }} + tag: ${{ steps.publish_release.outputs.tag }} + steps: + - uses: actions/checkout@v6 + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: latest + - name: Install semantic-release + run: | + npm i semantic-release/changelog + - name: Publish release + id: publish_release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + npx semantic-release + printf 'version=%s\ntag=%s\n' \ + "${{ needs.release-plan.outputs.version }}" \ + "${{ needs.release-plan.outputs.tag }}" >> "$GITHUB_OUTPUT" draft-binary-release: name: 📦 Pack release - needs: [semantic-release, win-build, osx-build, linux-build] - if: ${{ needs.semantic-release.outputs.release == 'True' }} + needs: [release-plan, publish-release, win-build, osx-build, linux-build] + if: ${{ needs.release-plan.outputs.release == 'True' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 @@ -172,7 +216,7 @@ frei0r-bin - name: create compressed archives run: | - dst=frei0r-${{ needs.semantic-release.outputs.version }}_win64 + dst=frei0r-${{ needs.publish-release.outputs.version }}_win64 mkdir -p $dst/filter $dst/generator $dst/mixer2 $dst/mixer3 find frei0r-bin/release-win64-filter -type f -name '*.dll' -exec cp {} $dst/filter \; find frei0r-bin/release-win64-generator -type f -name '*.dll' -exec cp {} $dst/generator \; @@ -183,10 +227,11 @@ cp ChangeLog $dst/ChangeLog.txt cp AUTHORS.md $dst/AUTHORS.txt cp include/frei0r.h include/frei0r.hpp $dst/ - echo "${{ needs.semantic-release.outputs.version }}" > $dst/VERSION.txt + cp frei0r-bin/release-win64-pkgconfig/frei0r.pc $dst/ + echo "${{ needs.publish-release.outputs.version }}" > $dst/VERSION.txt zip -r -9 $dst.zip $dst - dst=frei0r-${{ needs.semantic-release.outputs.version }}_osx + dst=frei0r-${{ needs.publish-release.outputs.version }}_osx mkdir -p $dst/filter $dst/generator $dst/mixer2 $dst/mixer3 find frei0r-bin/release-osx-filter -type f -name '*.so' -exec cp {} $dst/filter \; find frei0r-bin/release-osx-generator -type f -name '*.so' -exec cp {} $dst/generator \; @@ -197,10 +242,11 @@ cp ChangeLog $dst/ChangeLog.txt cp AUTHORS.md $dst/AUTHORS.txt cp include/frei0r.h include/frei0r.hpp $dst/ - echo "${{ needs.semantic-release.outputs.version }}" > $dst/VERSION.txt + cp frei0r-bin/release-osx-pkgconfig/frei0r.pc $dst/ + echo "${{ needs.publish-release.outputs.version }}" > $dst/VERSION.txt zip -r -9 $dst.zip $dst - dst=frei0r-${{ needs.semantic-release.outputs.version }}_linux + dst=frei0r-${{ needs.publish-release.outputs.version }}_linux mkdir -p $dst/filter $dst/generator $dst/mixer2 $dst/mixer3 find frei0r-bin/release-linux-filter -type f -name '*.so' -exec cp {} $dst/filter \; find frei0r-bin/release-linux-generator -type f -name '*.so' -exec cp {} $dst/generator \; @@ -211,7 +257,8 @@ cp ChangeLog $dst/ChangeLog.txt cp AUTHORS.md $dst/AUTHORS.txt cp include/frei0r.h include/frei0r.hpp $dst/ - echo "${{ needs.semantic-release.outputs.version }}" > $dst/VERSION.txt + cp frei0r-bin/release-linux-pkgconfig/frei0r.pc $dst/ + echo "${{ needs.publish-release.outputs.version }}" > $dst/VERSION.txt tar cvfz $dst.tar.gz $dst sha256sum *.zip *.tar.gz > SHA256SUMS.txt @@ -223,7 +270,7 @@ *.zip *.tar.gz SHA256SUMS.txt - tag_name: ${{ needs.semantic-release.outputs.version }} + tag_name: ${{ needs.publish-release.outputs.tag }} draft: true prerelease: false fail_on_unmatched_files: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 669bdba..9cb0f8d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,14 @@ cancel-in-progress: true jobs: + + # reuse: + # name: 🚨 REUSE Compliance + # runs-on: ubuntu-latest + # steps: + # - uses: actions/checkout@v6 + # - uses: fsfe/reuse-action@v1 + c-lint: name: 🚨 C lint runs-on: ubuntu-latest @@ -68,22 +76,46 @@ runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: install dependencies + with: + fetch-depth: 0 + - uses: hendrikmuhs/ccache-action@v1.2 + - name: Cache APT packages + uses: awalsh128/cache-apt-pkgs-action@latest + with: + packages: "${{ matrix.compiler }} cmake ninja-build libfreetype-dev libopencv-dev libcairo2-dev libgavl-dev" + - name: Detect new plugins + id: detect + if: github.event_name == 'pull_request' run: | - sudo apt-get update -qy - sudo apt-get install --no-install-recommends -y ${{ matrix.compiler }} cmake ninja-build libfreetype-dev libopencv-dev libcairo2-dev libgavl-dev + .github/scripts/detect-new-plugins.sh \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT" + - name: Print CI mode + run: | + echo "mode=${{ steps.detect.outputs.mode || 'full' }}" + echo "reason=${{ steps.detect.outputs.reason || 'push event uses the full suite' }}" - name: ${{ matrix.compiler }} initialize cmake build run: | mkdir -p build && cd build - cmake -G "Ninja" ../ + cmake -G "Ninja" -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache .. - name: ${{ matrix.compiler }} run ninja build + if: github.event_name != 'pull_request' || steps.detect.outputs.mode != 'targeted' run: | cd build && ninja + - name: ${{ matrix.compiler }} build new plugins + if: github.event_name == 'pull_request' && steps.detect.outputs.mode == 'targeted' + run: | + cd build + cmake --build . --target ${{ steps.detect.outputs.targets }} - name: ${{ matrix.compiler }} analyze plugins + if: github.event_name != 'pull_request' || steps.detect.outputs.mode != 'targeted' run: | - cd test && make - - name: ${{ matrix.compiler }} upload plugin analysis - uses: actions/upload-artifact@v6 - with: - name: release-plugin-analysis - path: test/*.json + cd test && make frei0r-asan && make check + - name: ${{ matrix.compiler }} analyze new plugins + if: github.event_name == 'pull_request' && steps.detect.outputs.mode == 'targeted' + run: | + cd test + make frei0r-asan + for plugin in ${{ steps.detect.outputs.plugin_paths }}; do + ./frei0r-run -d -p "../$plugin" + done diff --git a/BUILD.md b/BUILD.md index 07388a6..729ca1a 100644 --- a/BUILD.md +++ b/BUILD.md @@ -1,6 +1,13 @@ # Build instructions Frei0r can be built using CMake. + +Minimum toolchain expectations: + + + C compiler + + C++ compiler with C++11 support (required) + + CMake + + Ninja or Make The presence of optional libraries on the system will trigger compilation of extra plugins. These libraries are: @@ -17,21 +24,33 @@ It is recommended to use a separate `build` sub-folder. ``` -mkdir -p build -cd build && cmake ../ -make +cmake -S . -B build +cmake --build build ``` To disable face recognition plugins (recommended when using with MLT): ``` -mkdir -p build -cd build && cmake -DWITHOUT_FACERECOGNITION=ON ../ -make +cmake -S . -B build -DWITHOUT_FACERECOGNITION=ON +cmake --build build ``` -Also ninja and nmake are supported through cmake: +Ninja and nmake are also supported through CMake: ``` -cmake -G 'Ninja' ../ -cmake -G 'NMake Makefiles' ../ +cmake -S . -B build -G 'Ninja' +cmake -S . -B build -G 'NMake Makefiles' ``` +Top-level shorthand targets are available through `GNUmakefile`: +``` +make release-gcc-ninja +make debug-gcc +``` + +Runtime test utilities: +``` +cd test +make frei0r-asan # builds ./frei0r-run with ASAN +make check # loads and runs all built plugins under ../build/src +make frei0r-meta +make scan-meta +``` diff --git a/CMakeLists.txt b/CMakeLists.txt index 10bc05c..e378e34 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,8 +2,18 @@ list (APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) -project (frei0r) -set (VERSION 2.5.1) +set (FREI0R_VERSION "3.0.0" CACHE STRING "Project version in SemVer format") +if (DEFINED ENV{FREI0R_VERSION} AND NOT "$ENV{FREI0R_VERSION}" STREQUAL "") + set (FREI0R_VERSION "$ENV{FREI0R_VERSION}") +endif () +string (REGEX REPLACE "^v" "" FREI0R_VERSION "${FREI0R_VERSION}") +if (NOT FREI0R_VERSION MATCHES "^[0-9]+\\.[0-9]+\\.[0-9]+$") + message (FATAL_ERROR "FREI0R_VERSION must be a SemVer string like 3.0.0") +endif () + +project (frei0r VERSION ${FREI0R_VERSION}) +set (CMAKE_CXX_STANDARD 11) +set (CMAKE_CXX_STANDARD_REQUIRED True) include(GNUInstallDirs) diff --git a/GNUmakefile b/GNUmakefile new file mode 100644 index 0000000..a333c5c --- /dev/null +++ b/GNUmakefile @@ -0,0 +1,37 @@ +all: release-gcc-ninja + +debug: debug-gcc + +release-gcc: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=Release .. + cd build && make + +release-gcc-ninja: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=Release -G 'Ninja' .. + cd build && ninja + +release-clang: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release .. + cd build && make + +release-clang-ninja: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Release -G 'Ninja' .. + cd build && ninja + +debug-gcc: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=gcc -DCMAKE_CXX_COMPILER=g++ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS_DEBUG='-ggdb -fno-omit-frame-pointer -fsanitize=address' -DCMAKE_C_FLAGS_DEBUG='-ggdb -fno-omit-frame-pointer -fsanitize=address' .. + cd build && make + + +debug-clang-ninja: + mkdir -p build + cd build && cmake -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS_DEBUG='-ggdb -fno-omit-frame-pointer -fsanitize=address' -DCMAKE_C_FLAGS_DEBUG='-ggdb -fno-omit-frame-pointer -fsanitize=address' -G 'Ninja' .. + cd build && ninja + +clean: + rm -rf build diff --git a/README.md b/README.md index c8238fb..e781765 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,22 @@ For details see the [BUILD](/BUILD.md) file. +### Quick build and test + +```sh +cmake -S . -B build -G Ninja +cmake --build build +cd test && make frei0r-asan && make check +``` + +### Metadata scan utility + +The metadata scanner binary is `test/frei0r-meta` (previously `frei0r-info`): + +```sh +cd test && make frei0r-meta && make scan-meta +``` + ### MS / Windows We distribute official builds of frei0r plugins as .dll for the Win64 platform from the releases page. @@ -86,7 +102,7 @@ A [frei0r Brew formula](https://formulae.brew.sh/formula/frei0r) is available. -Official builds of frei0r plugins as .dlsym for the Apple/OSX platform will be soon included in the releases page. +Official macOS release artifacts are distributed from the releases page. # Documentation @@ -160,4 +176,3 @@ For a full list of contributors and the project history, see the file [AUTHORS](/AUTHORS), the [ChangeLog](/ChangeLog) and the project web page: https://frei0r.dyne.org - diff --git a/frei0r.pc.in b/frei0r.pc.in index 3bd60da..efafae0 100644 --- a/frei0r.pc.in +++ b/frei0r.pc.in @@ -5,7 +5,6 @@ Name: frei0r Description: minimalistic plugin API for video effects -Version: @VERSION@ +Version: @PROJECT_VERSION@ Libs: Cflags: -I${includedir} - diff --git a/include/frei0r.hpp b/include/frei0r.hpp index a340bae..dd17cdd 100644 --- a/include/frei0r.hpp +++ b/include/frei0r.hpp @@ -91,12 +91,15 @@ param_ptrs.push_back(&p_loc); s_params.push_back(param_info(name,desc,F0R_PARAM_STRING)); } - - + + void get_param_value(f0r_param_t param, int param_index) { + if (!param || param_index < 0 || param_index >= (int)param_ptrs.size()) + return; + void* ptr = param_ptrs[param_index]; - + switch (s_params[param_index].m_type) { case F0R_PARAM_BOOL : @@ -125,8 +128,11 @@ void set_param_value(f0r_param_t param, int param_index) { + if (!param || param_index < 0 || param_index >= (int)param_ptrs.size()) + return; + void* ptr = param_ptrs[param_index]; - + switch (s_params[param_index].m_type) { case F0R_PARAM_BOOL : @@ -146,13 +152,16 @@ = *static_cast(param); break; case F0R_PARAM_STRING: - *static_cast(ptr) - = *static_cast(param); + { + f0r_param_string str = *static_cast(param); + if (str) + *static_cast(ptr) = str; break; } - - } - + } + + } + virtual void update(double time, uint32_t* out, const uint32_t* in1, @@ -297,6 +306,9 @@ void f0r_get_param_info(f0r_param_info_t* info, int param_index) { + if (!info || param_index < 0 || param_index >= (int)frei0r::s_params.size()) + return; + info->name=frei0r::s_params[param_index].m_name.c_str(); info->type=frei0r::s_params[param_index].m_type; info->explanation=frei0r::s_params[param_index].m_desc.c_str(); @@ -316,16 +328,18 @@ delete static_cast(instance); } -void f0r_set_param_value(f0r_instance_t instance, +void f0r_set_param_value(f0r_instance_t instance, f0r_param_t param, int param_index) { - static_cast(instance)->set_param_value(param, param_index); + if (instance) + static_cast(instance)->set_param_value(param, param_index); } void f0r_get_param_value(f0r_instance_t instance, f0r_param_t param, int param_index) { - static_cast(instance)->get_param_value(param, param_index); + if (instance) + static_cast(instance)->get_param_value(param, param_index); } void f0r_update2(f0r_instance_t instance, double time, @@ -347,4 +361,3 @@ { f0r_update2(instance, time, inframe, 0, 0, outframe); } - diff --git a/src/filter/CMakeLists.txt b/src/filter/CMakeLists.txt index 6d52964..b0f22a4 100644 --- a/src/filter/CMakeLists.txt +++ b/src/filter/CMakeLists.txt @@ -27,6 +27,7 @@ add_subdirectory (brightness) add_subdirectory (bw0r) add_subdirectory (cartoon) +add_subdirectory (camerashake) add_subdirectory (cluster) add_subdirectory (colgate) add_subdirectory (coloradj) @@ -58,7 +59,7 @@ add_subdirectory (heatmap0r) add_subdirectory (hueshift0r) add_subdirectory (invert0r) -add_subdirectory (kaleid0sc0pe) +add_subdirectory (kaleid0sc0pe) add_subdirectory (keyspillm0pup) add_subdirectory (lenscorrection) add_subdirectory (letterb0xed) @@ -103,3 +104,4 @@ add_subdirectory (twolay0r) add_subdirectory (vertigo) add_subdirectory (vignette) +add_subdirectory (water) diff --git a/src/filter/autothresh0ld/autothresh0ld.c b/src/filter/autothresh0ld/autothresh0ld.c index 4941d12..99deda3 100644 --- a/src/filter/autothresh0ld/autothresh0ld.c +++ b/src/filter/autothresh0ld/autothresh0ld.c @@ -186,7 +186,9 @@ } } -void f0r_destruct(f0r_instance_t s) +void f0r_destruct(f0r_instance_t inst) { + s0ft0tsu_t* s = inst; + free(s->lumaframe); free(s); } diff --git a/src/filter/camerashake/CMakeLists.txt b/src/filter/camerashake/CMakeLists.txt new file mode 100644 index 0000000..48aa65c --- /dev/null +++ b/src/filter/camerashake/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.10) +project(CameraShakePlugin) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) + +# Compile as shared library +add_library(camerashake MODULE camerashake.cpp) + +# Remove the "lib" prefix so that Frei0r can read it correctly +set_target_properties(camerashake PROPERTIES PREFIX "") diff --git a/src/filter/camerashake/camerashake.cpp b/src/filter/camerashake/camerashake.cpp new file mode 100644 index 0000000..86fdf7a --- /dev/null +++ b/src/filter/camerashake/camerashake.cpp @@ -0,0 +1,193 @@ +/* camerashake.cpp + * Copyright (C) 2026 PakeMPC + * This file is a Frei0r plugin. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + */ + + +#include +#include +#if defined(_MSC_VER) +#define _USE_MATH_DEFINES +#endif +#include + +extern "C" { + #include + + struct CameraShakeInstance { + double amp_x, amp_y, rotation, zoom, speed, opacity, blur; + float bg_r, bg_g, bg_b; + unsigned int width, height; + }; + + int f0r_init(void) { return 1; } + void f0r_deinit(void) {} + + void f0r_get_plugin_info(f0r_plugin_info_t* info) { + info->name = "Camera Shake Ultimate"; + info->author = "PakeMPC"; + info->plugin_type = F0R_PLUGIN_TYPE_FILTER; + info->color_model = F0R_COLOR_MODEL_RGBA8888; + info->frei0r_version = FREI0R_MAJOR_VERSION; + info->major_version = 2; info->minor_version = 0; + info->num_params = 8; + info->explanation = "Camera movement effect with rotation, opacity, and blur."; + } + +void f0r_get_param_info(f0r_param_info_t* info, int param_index) { + if (param_index == 0) { info->name = "amplitude_x"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 1) { info->name = "amplitude_y"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 2) { info->name = "rotation"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 3) { info->name = "zoom"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 4) { info->name = "speed"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 5) { info->name = "opacity"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 6) { info->name = "blur"; info->type = F0R_PARAM_DOUBLE; } + else if (param_index == 7) { info->name = "bg_color"; info->type = F0R_PARAM_COLOR; } + } + + f0r_instance_t f0r_construct(unsigned int width, unsigned int height) { + CameraShakeInstance* inst = new CameraShakeInstance(); + inst->amp_x = 50.0; inst->amp_y = 50.0; inst->rotation = 10.0; + inst->zoom = 100.0; inst->speed = 50.0; inst->opacity = 100.0; inst->blur = 0.0; + inst->bg_r = 0.0f; inst->bg_g = 0.0f; inst->bg_b = 0.0f; + inst->width = width; inst->height = height; + return (f0r_instance_t)inst; + } + + void f0r_destruct(f0r_instance_t instance) { delete (CameraShakeInstance*)instance; } + + void f0r_set_param_value(f0r_instance_t instance, f0r_param_t param, int param_index) { + CameraShakeInstance* inst = (CameraShakeInstance*)instance; + double val = *((double*)param); + if (param_index == 0) inst->amp_x = val; + else if (param_index == 1) inst->amp_y = val; + else if (param_index == 2) inst->rotation = val; + else if (param_index == 3) inst->zoom = val; + else if (param_index == 4) inst->speed = val; + else if (param_index == 5) inst->opacity = val; + else if (param_index == 6) inst->blur = val; + else if (param_index == 7) { + f0r_param_color_t* color = (f0r_param_color_t*)param; + inst->bg_r = color->r; inst->bg_g = color->g; inst->bg_b = color->b; + } + } + +void f0r_get_param_value(f0r_instance_t instance, f0r_param_t param, int param_index) { + CameraShakeInstance* inst = (CameraShakeInstance*)instance; + if (param_index == 0) *((double*)param) = inst->amp_x; + else if (param_index == 1) *((double*)param) = inst->amp_y; + else if (param_index == 2) *((double*)param) = inst->rotation; + else if (param_index == 3) *((double*)param) = inst->zoom; + else if (param_index == 4) *((double*)param) = inst->speed; + else if (param_index == 5) *((double*)param) = inst->opacity; + else if (param_index == 6) *((double*)param) = inst->blur; + else if (param_index == 7) { + f0r_param_color_t* color = (f0r_param_color_t*)param; + color->r = inst->bg_r; color->g = inst->bg_g; color->b = inst->bg_b; + } + } + + void f0r_update(f0r_instance_t instance, double time, const uint32_t* inframe, uint32_t* outframe) { + CameraShakeInstance* inst = (CameraShakeInstance*)instance; + int w = inst->width; + int h = inst->height; + + double speed_factor = inst->speed; + + // 1. Zoom (0 to 500) mapped to scale (0.001 to 5.0) + double scale = inst->zoom / 100.0; + if (scale < 0.001) scale = 0.001; // Avoid division by zero + + // 2. Shake X and Y (using the direct values ​​up to 500px) + double shake_x = sin(time * speed_factor) * cos(time * speed_factor * 0.73) * inst->amp_x; + double shake_y = sin(time * speed_factor * 0.89) * cos(time * speed_factor * 1.1) * inst->amp_y; + + // 3. Rotation + double max_angle = (inst->rotation / 100.0) * (M_PI / 4.0); // Maximum 45 degree + double theta = sin(time * speed_factor * 1.15) * max_angle; + double cos_t = cos(-theta); + double sin_t = sin(-theta); + + // 4. Blur and Opacity Parameters + int blur_radius = (int)(inst->blur / 5.0); // Range of 0 to 20 pixels radius + double alpha_mult = inst->opacity / 100.0; + + double cx = w / 2.0; + double cy = h / 2.0; + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + + // Inverse mapping of rotation and scale matrix + double dx = (x - cx) / scale; + double dy = (y - cy) / scale; + + double rx = dx * cos_t - dy * sin_t; + double ry = dx * sin_t + dy * cos_t; + + int base_src_x = (int)(cx + rx + shake_x); + int base_src_y = (int)(cy + ry + shake_y); + + uint8_t* out_ptr = (uint8_t*)&outframe[y * w + x]; + + // Check limits + if (base_src_x < 0 || base_src_x >= w || base_src_y < 0 || base_src_y >= h) { + out_ptr[0] = (uint8_t)(inst->bg_r * 255.0f); + out_ptr[1] = (uint8_t)(inst->bg_g * 255.0f); + out_ptr[2] = (uint8_t)(inst->bg_b * 255.0f); + out_ptr[3] = (uint8_t)(255 * alpha_mult); + continue; + } + + // If there's NO blur, paint directly + if (blur_radius == 0) { + uint8_t* in_ptr = (uint8_t*)&inframe[base_src_y * w + base_src_x]; + out_ptr[0] = in_ptr[0]; out_ptr[1] = in_ptr[1]; out_ptr[2] = in_ptr[2]; + out_ptr[3] = (uint8_t)(in_ptr[3] * alpha_mult); + } + // If there is blur, do a "Fast Cross-Blur" + else { + int sum_r = 0, sum_g = 0, sum_b = 0, sum_a = 0; + int samples = 0; + + // Horizontal and vertical sampling (cross shape for yield) + for (int i = -blur_radius; i <= blur_radius; i += 2) { + // Horizontal + int sx = base_src_x + i; + if (sx >= 0 && sx < w) { + uint8_t* p = (uint8_t*)&inframe[base_src_y * w + sx]; + sum_r += p[0]; sum_g += p[1]; sum_b += p[2]; sum_a += p[3]; + samples++; + } + // Vertical + int sy = base_src_y + i; + if (sy >= 0 && sy < h && i != 0) { // i!=0 to avoid adding the center twice + uint8_t* p = (uint8_t*)&inframe[sy * w + base_src_x]; + sum_r += p[0]; sum_g += p[1]; sum_b += p[2]; sum_a += p[3]; + samples++; + } + } + + out_ptr[0] = sum_r / samples; + out_ptr[1] = sum_g / samples; + out_ptr[2] = sum_b / samples; + out_ptr[3] = (uint8_t)((sum_a / samples) * alpha_mult); + } + } + } + } +} diff --git a/src/filter/curves/curves.c b/src/filter/curves/curves.c index f72d066..c78dfb8 100644 --- a/src/filter/curves/curves.c +++ b/src/filter/curves/curves.c @@ -331,6 +331,7 @@ break; case 3: inst->pointNumber = floor(*((f0r_param_double *)param) * 10); + inst->pointNumber = MIN(inst->pointNumber, 5); break; case 4: inst->formula = *((f0r_param_double *)param); @@ -738,7 +739,7 @@ double *points = (double*)calloc(inst->pointNumber * 2, sizeof(double)); int i = inst->pointNumber * 2; //copy point values - while(--i > 0) + while(--i >= 0) points[i] = inst->points[i]; //sort point values by X component for(i = 1; i < inst->pointNumber; i++) @@ -799,7 +800,7 @@ points = (double*)calloc(inst->pointNumber * 2, sizeof(double)); i = inst->pointNumber * 2; //copy point values - while(--i > 0) + while(--i >= 0) points[i] = inst->points[i]; //sort point values by X component for(i = 1; i < inst->pointNumber; i++) diff --git a/src/filter/delay0r/delay0r.cpp b/src/filter/delay0r/delay0r.cpp index 758f30e..ce0f92d 100644 --- a/src/filter/delay0r/delay0r.cpp +++ b/src/filter/delay0r/delay0r.cpp @@ -16,7 +16,7 @@ ~delay0r() { - for (std::list< std::pair< double, unsigned int* > >::iterator i=buffer.begin(); i != buffer.end(); ++i) + for (std::list< std::pair< double, unsigned int* > >::iterator i=buffer.begin(); i != buffer.end(); ) { delete[] i->second; i=buffer.erase(i); @@ -29,7 +29,7 @@ { unsigned int* reusable = 0; // remove old frames - for (std::list< std::pair< double, unsigned int* > >::iterator i=buffer.begin(); i != buffer.end(); ++i) + for (std::list< std::pair< double, unsigned int* > >::iterator i=buffer.begin(); i != buffer.end(); ) { if (i->first < (time - delay) || i->first >= time) { @@ -40,6 +40,10 @@ reusable = i->second; i=buffer.erase(i); + } + else + { + ++i; } } diff --git a/src/filter/gateweave/gateweave.c b/src/filter/gateweave/gateweave.c index eef3935..9957660 100644 --- a/src/filter/gateweave/gateweave.c +++ b/src/filter/gateweave/gateweave.c @@ -149,7 +149,9 @@ uint32_t v = 0; for(unsigned int i = 0; i < w * h; i++) { - if(!(i + shift_component_x < 0 || i + shift_component_x >= w * h || i + shift_component_x + shift_component_y < 0 || i + shift_component_x + shift_component_y >= w * h)) + if(i + shift_component_x >= 0 && i + shift_component_x < w * h && + i + shift_component_y >= 0 && i + shift_component_y < w * h && + i + shift_component_x + shift_component_y >= 0 && i + shift_component_x + shift_component_y < w * h) { if(larger_x_comp) { diff --git a/src/filter/kaleid0sc0pe/kaleid0sc0pe.cpp b/src/filter/kaleid0sc0pe/kaleid0sc0pe.cpp index 4a40882..5f7ed57 100644 --- a/src/filter/kaleid0sc0pe/kaleid0sc0pe.cpp +++ b/src/filter/kaleid0sc0pe/kaleid0sc0pe.cpp @@ -23,6 +23,7 @@ * */ #include "kaleid0sc0pe.h" +#include "frei0r/math.h" #include #include #ifndef NO_FUTURE @@ -439,11 +440,17 @@ __m128 ge_height = _mm_cmpge_ps(source_y, m_sse_height); source_y = _mm_or_ps(_mm_and_ps(_mm_sub_ps(m_sse_height, _mm_sub_ps(source_y, m_sse_height)), ge_height), _mm_andnot_ps(ge_height,source_y)); - __m128i source_xi = _mm_cvttps_epi32(_mm_min_ps(source_x, _mm_sub_ps(m_sse_width, m_sse_ps_1))); - __m128i source_yi = _mm_cvttps_epi32(_mm_min_ps(source_y, _mm_sub_ps(m_sse_height, m_sse_ps_1))); + __m128i source_xi = _mm_cvttps_epi32(_mm_min_ps(_mm_max_ps(source_x, _mm_setzero_ps()), _mm_sub_ps(m_sse_width, m_sse_ps_1))); + __m128i source_yi = _mm_cvttps_epi32(_mm_min_ps(_mm_max_ps(source_y, _mm_setzero_ps()), _mm_sub_ps(m_sse_height, m_sse_ps_1))); std::int32_t* sx = reinterpret_cast(&source_xi); std::int32_t* sy = reinterpret_cast(&source_yi); + + for (int i = 0; i < 4; ++i) { + sx[i] = CLAMP(sx[i], 0, static_cast(m_width) - 1); + sy[i] = CLAMP(sy[i], 0, static_cast(m_height) - 1); + } + std::memcpy(out, lookup(block->in_frame, sx[0], sy[0]), m_pixel_size); out += m_pixel_size; std::memcpy(out, lookup(block->in_frame, sx[1], sy[1]), m_pixel_size); @@ -511,6 +518,8 @@ } else if (source_y > m_height - 10e-4f) { source_y = m_height - (source_y - m_height + 10e-4f); } + source_x = CLAMP(source_x, 0.0f, static_cast(m_width - 1)); + source_y = CLAMP(source_y, 0.0f, static_cast(m_height - 1)); std::memcpy(out, lookup(block->in_frame, static_cast(source_x), static_cast(source_y)), m_pixel_size); } else { process_bg(source_x, source_y, block->in_frame, out); diff --git a/src/filter/keyspillm0pup/keyspillm0pup.c b/src/filter/keyspillm0pup/keyspillm0pup.c index daa78c0..284e2fe 100644 --- a/src/filter/keyspillm0pup/keyspillm0pup.c +++ b/src/filter/keyspillm0pup/keyspillm0pup.c @@ -878,7 +878,7 @@ break; case 2: //Mask type (list) tmpch = (*(f0r_param_string*)parm); - if (strcmp(p->liststr, tmpch)) { + if (tmpch && strcmp(p->liststr, tmpch)) { p->liststr = realloc( p->liststr, strlen(tmpch) + 1 ); strcpy( p->liststr, tmpch ); } @@ -910,7 +910,7 @@ break; case 7: //Operation 1 (list) tmpch = (*(f0r_param_string*)parm); - if (strcmp(p->liststr, tmpch)) { + if (tmpch && strcmp(p->liststr, tmpch)) { p->liststr = realloc( p->liststr, strlen(tmpch) + 1 ); strcpy( p->liststr, tmpch ); } @@ -927,7 +927,7 @@ break; case 9: //Operation 2 (list) tmpch = (*(f0r_param_string*)parm); - if (strcmp(p->liststr, tmpch)) { + if (tmpch && strcmp(p->liststr, tmpch)) { p->liststr = realloc( p->liststr, strlen(tmpch) + 1 ); strcpy( p->liststr, tmpch ); } diff --git a/src/filter/pixs0r/pixs0r.cc b/src/filter/pixs0r/pixs0r.cc index 66a5fff..cd21224 100644 --- a/src/filter/pixs0r/pixs0r.cc +++ b/src/filter/pixs0r/pixs0r.cc @@ -1,4 +1,5 @@ #include +#include struct pixshift0r { @@ -7,39 +8,45 @@ unsigned int m_shift_intensity; unsigned int m_block_height; - + // If m_block_height == 0, then these bound block heights unsigned int m_block_height_min; unsigned int m_block_height_max; - // FIXME: It might be better to make it global variable. - std::random_device m_rng_dev; - - std::uniform_int_distribution m_shift_rng; + std::mt19937 m_rng; + + std::uniform_int_distribution m_shift_rng; std::uniform_int_distribution m_block_height_rng; pixshift0r(unsigned int width, unsigned int height) - : m_width(width), m_height(height), m_block_height(0) {} + : m_width(width), m_height(height), m_block_height(0), + m_shift_intensity(0), m_block_height_min(1), m_block_height_max(1), + m_rng(std::random_device{}()), + m_shift_rng(0, 0), m_block_height_rng(1, 1) {} void process(const uint32_t *inframe, uint32_t *outframe) { for (unsigned int b = 0; b < m_height;) { - unsigned int block_height = m_block_height ? m_block_height : m_block_height_rng(m_rng_dev); + unsigned int block_height = m_block_height ? m_block_height : m_block_height_rng(m_rng); // Number of rows to shift is either block size or // what we can *safely* operate on (avoids corruption). block_height = std::min(block_height, m_height - b); - long long shift = m_shift_rng(m_rng_dev); + // Ensure we always make progress + if (block_height == 0) + block_height = 1; + + int64_t shift = m_shift_intensity ? m_shift_rng(m_rng) : 0; for (unsigned int j = 0; j < block_height; ++j) { - // NOTE: Faulty implementations, such as FFmpeg don't - // check for (width % 8 == 0 && height % 8 == 0). - // - // A possible workaround for all filters, is a scale filter: - // (e.g "scale=round(in_w/8)*8:round(in_h/8)*8:flags=neighbor"). + // NOTE: Faulty implementations, such as FFmpeg don't + // check for (width % 8 == 0 && height % 8 == 0). + // + // A possible workaround for all filters, is a scale filter: + // (e.g "scale=round(in_w/8)*8:round(in_h/8)*8:flags=neighbor"). // // https://ffmpeg.org/doxygen/trunk/vf__frei0r_8c_source.html#l00352 size_t pitch = static_cast(b + j) * m_width; @@ -73,7 +80,7 @@ this->m_block_height_max = max_bh; auto rng_params = decltype(this->m_block_height_rng)::param_type(min_bh, max_bh); - + this->m_block_height_rng.param(rng_params); } @@ -81,10 +88,10 @@ { this->m_shift_intensity = shift_intensity; - long long intensity = static_cast(shift_intensity); + int64_t intensity = static_cast(shift_intensity); auto rng_params = decltype(this->m_shift_rng)::param_type(-intensity, intensity); - + this->m_shift_rng.param(rng_params); } }; @@ -102,8 +109,8 @@ pixshift0r *instance = new pixshift0r(width, height); instance->set_shift_intensity(width / 100); - instance->set_block_height_range(height / 100, height / 10); - + instance->set_block_height_range(std::max(1u, height / 100), std::max(1u, height / 10)); + return instance; } @@ -130,7 +137,7 @@ info->type = F0R_PARAM_DOUBLE; info->explanation = "Aggressiveness of row/column-shifting"; break; - + case 1: info->name = "block_height"; info->type = F0R_PARAM_DOUBLE; @@ -154,7 +161,7 @@ void f0r_get_param_value( f0r_instance_t instance, f0r_param_t param, - int param_index + int param_index ) { pixshift0r* context = (pixshift0r*)instance; @@ -178,7 +185,7 @@ void f0r_set_param_value( f0r_instance_t instance, f0r_param_t param, - int param_index + int param_index ) { pixshift0r* context = (pixshift0r*)instance; @@ -203,7 +210,7 @@ f0r_instance_t instance, double, const uint32_t *inframe, - uint32_t *outframe + uint32_t *outframe ) { static_cast(instance)->process(inframe, outframe); @@ -211,7 +218,7 @@ void f0r_deinit() {} -void f0r_destruct (f0r_instance_t instance) +void f0r_destruct (f0r_instance_t instance) { delete static_cast(instance); } diff --git a/src/filter/vertigo/vertigo.c b/src/filter/vertigo/vertigo.c index bb59324..b04bf79 100644 --- a/src/filter/vertigo/vertigo.c +++ b/src/filter/vertigo/vertigo.c @@ -243,7 +243,7 @@ { i = (oy>>16)*w + (ox>>16); if(i<0) i = 0; - if(i>=inst->pixels) i = inst->pixels; + if(i>=inst->pixels) i = inst->pixels - 1; v = inst->current_buffer[i] & 0xfcfcff; alpha = *src & 0xff000000; v = (v * 3) + ((*src++) & 0xfcfcff); diff --git a/src/generator/test_pat/test_pat_G.c b/src/generator/test_pat/test_pat_G.c index 4e592f6..cd942e0 100644 --- a/src/generator/test_pat/test_pat_G.c +++ b/src/generator/test_pat/test_pat_G.c @@ -46,6 +46,7 @@ #include #include "frei0r.h" +#include "frei0r/math.h" //---------------------------------------------------------- @@ -158,6 +159,8 @@ if (size<1) size=1; kx=size; ky=size; kx=kx/ar; //kao aspect!=1 (anamorph) +kx = MAX(kx, 1); +ky = MAX(ky, 1); black=0; white=255; diff --git a/test/Makefile b/test/Makefile index 70e924c..c1ad15b 100644 --- a/test/Makefile +++ b/test/Makefile @@ -2,21 +2,62 @@ PLUGINDIR ?= ../build/src -all: build scan-plugins +CXX ?= g++ -scan-plugins: - @$(if $(wildcard ${PLUGINDIR}),,>&2 echo "Scan dir not found: ${PLUGINDIR}" && exit 1) - @find ${PLUGINDIR} -type f -name '*.so' -exec ./frei0r-info {} \; > tmp.json +DEBUG_FLAGS := -O0 -g -ggdb -Wl,-undefined -Wl,dynamic_lookup -fsanitize=address -fsanitize-recover=address -fno-omit-frame-pointer -fsanitize-address-use-after-scope + +CI_OPT_FLAGS := -O3 -ffast-math -flto -march=native -pipe + +# CI runs: make && make check (see .github/workflow) +# so the 'all' target is built in CI +all: frei0r-meta frei0r-asan + @echo "Test targets available:" + @echo "frei0r-run :: build headless test utility (default)" + @echo "frei0r-asan :: build ASAN test utility" + @echo "frei0r-gui :: build graphical test utility" + @echo "check :: run the built test on all plugins" + @echo "frei0r-meta :: build metadata plugin scanner" + @echo "scan-meta :: scan all plugins and produce metadata" + +frei0r-run: frei0r-run.c + @printf 'Build frei0r plugin test run utility\n' + $(CXX) $(CI_OPT_FLAGS) -I../include -o frei0r-run frei0r-run.c -ldl + +frei0r-asan: frei0r-run.c + @printf 'Build frei0r plugin test run utility\n' + $(CXX) $(DEBUG_FLAGS) -I../include -o frei0r-run frei0r-run.c -ldl + +frei0r-gui: frei0r-run.c test-pattern.c test-pattern.h + @printf 'Build frei0r plugin test run utility\n' + $(CXX) $(CI_OPT_FLAGS) -I../include -o frei0r-run frei0r-run.c test-pattern.c -ldl -lm -lX11 -DGUI + +check: frei0r-run + @if [ ! -d "${PLUGINDIR}" ]; then printf 'Scan dir not found: %s\n' "${PLUGINDIR}" >&2; exit 1; fi + @find "${PLUGINDIR}" -type f -name '*.so' | \ + while IFS= read -r f; do \ + ./frei0r-run -d -p "$$f" || break; done + +frei0r-meta: frei0r-meta.c + @printf 'Build frei0r meta-data parsing utility\n' + ${CC} -o frei0r-meta -ggdb frei0r-meta.c ${INCLUDES} + +scan-meta: frei0r-meta + @if [ ! -d "${PLUGINDIR}" ]; then printf 'Scan dir not found: %s\n' "${PLUGINDIR}" >&2; exit 1; fi + @find ${PLUGINDIR} -type f -name '*.so' -exec ./frei0r-meta {} \; > tmp.json @echo "[" > frei0r-plugin-list.json @head -n -1 tmp.json >> frei0r-plugin-list.json @echo "}\n]" >> frei0r-plugin-list.json @rm tmp.json - $(info frei0r-plugin-list.json) + @printf 'frei0r-plugin-list.json\n' -build: - @${CC} -o frei0r-info -ggdb frei0r-info.c ${INCLUDES} + + +generate-metadata: EXTENSION ?= so +generate-metadata: + @if [ ! -d "${PLUGINDIR}" ]; then printf 'Scan dir not found: %s\n' "${PLUGINDIR}" >&2; exit 1; fi + sh extract-plugin-info.sh ${EXTENSION} ${PLUGINDIR} clean: rm -f *.o - rm -f frei0r-info + rm -f frei0r-run frei0r-meta rm -f *.json diff --git a/test/extract-plugin-info.sh b/test/extract-plugin-info.sh new file mode 100755 index 0000000..3f0d23f --- /dev/null +++ b/test/extract-plugin-info.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +[ -r frei0r-meta ] || gmake +tmp=`mktemp` +find "${2}" -name "*.$1" | + while read -r line; do + file=`basename $line` + dir=`dirname $line` + name=`echo $file | cut -d. -f1` + ./frei0r-meta "$line" > $tmp + mv $tmp "$dir/$name.json" + ls "$dir/$name.json" + done +rm -f $tmp diff --git a/test/frei0r-info.c b/test/frei0r-info.c deleted file mode 100644 index de03085..0000000 --- a/test/frei0r-info.c +++ /dev/null @@ -1,103 +0,0 @@ - -#include -#include -#include -#include -#include -#include - -#include - -// 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); - -int main(int argc, char **argv) { - - // instance frei0r pointers - static void *dl_handle; - static f0r_init_f f0r_init; - static f0r_init_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; - - int c; - - if(argc<2) exit(1); - const char *file = basename(argv[1]); - const char *dir = dirname(argv[1]); - 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); - if(!dl_handle) { - fprintf(stderr,"error: %s\n",dlerror()); - exit(1); - } - // get plugin function calls - f0r_init = dlsym(dl_handle,"f0r_init"); - f0r_deinit = dlsym(dl_handle,"f0r_deinit"); - f0r_get_plugin_info = dlsym(dl_handle,"f0r_get_plugin_info"); - f0r_get_param_info = dlsym(dl_handle,"f0r_get_param_info"); - // always initialize plugin first - f0r_init(); - // get info about plugin - f0r_get_plugin_info(&pi); - fprintf(stdout, - "{\n \"name\":\"%s\",\n \"type\":\"%s\",\n \"author\":\"%s\",\n" - " \"explanation\":\"%s\",\n \"color_model\":\"%s\",\n" - " \"frei0r_version\":\"%d\",\n \"version\":\"%d.%d\",\n \"num_params\":\"%d\"", - 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", - pi.author, pi.explanation, - 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", - pi.frei0r_version, pi.major_version, pi.minor_version, pi.num_params); - - /* // check icon */ - /* char icon[256]; */ - /* char *dot = rindex(file, '.'); */ - /* *dot = 0x0; */ - /* snprintf(icon,255,"%s/%s.png",dir,file); */ - /* FILE *icon_fd = fopen(icon,"r"); */ - /* if(icon_fd) { */ - /* fprintf(stderr," icon found: %s\n",icon); */ - /* } */ - - // get info about params - if(pi.num_params>0) { - fprintf(stdout,",\n \"params\":[\n"); - for(c=0; cc+1) { - fprintf(stdout,",\n"); - } else { - fprintf(stdout,"\n"); - } - } - fprintf(stdout," ]\n"); - } - fprintf(stdout,"\n},\n"); - fflush(stdout); - f0r_deinit(); - dlclose(dl_handle); - exit(0); -} diff --git a/test/frei0r-meta.c b/test/frei0r-meta.c new file mode 100644 index 0000000..44b7776 --- /dev/null +++ b/test/frei0r-meta.c @@ -0,0 +1,122 @@ +/* This file is part of frei0r (https://frei0r.dyne.org) + * + * Copyright (C) 2024-2025 Dyne.org foundation + * designed, written and maintained by Denis Roio + * + * 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 . + * + */ + +#include +#include +#include +#include +#include +#include + +#include + +// 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); + +int main(int argc, char **argv) { + + // instance frei0r pointers + static void *dl_handle; + static f0r_init_f f0r_init; + static f0r_init_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; + + int c; + + if(argc<2) exit(1); + const char *file = basename(argv[1]); + const char *dir = dirname(argv[1]); + 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); + if(!dl_handle) { + fprintf(stderr,"error: %s\n",dlerror()); + exit(1); + } + // get plugin function calls + f0r_init = dlsym(dl_handle,"f0r_init"); + f0r_deinit = dlsym(dl_handle,"f0r_deinit"); + f0r_get_plugin_info = dlsym(dl_handle,"f0r_get_plugin_info"); + f0r_get_param_info = dlsym(dl_handle,"f0r_get_param_info"); + // always initialize plugin first + f0r_init(); + // get info about plugin + f0r_get_plugin_info(&pi); + fprintf(stdout, + "{\n \"name\":\"%s\",\n \"type\":\"%s\",\n \"author\":\"%s\",\n" + " \"explanation\":\"%s\",\n \"color_model\":\"%s\",\n" + " \"frei0r_version\":\"%d\",\n \"version\":\"%d.%d\",\n \"num_params\":\"%d\"", + 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", + pi.author, pi.explanation, + 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", + pi.frei0r_version, pi.major_version, pi.minor_version, pi.num_params); + + /* // check icon */ + /* char icon[256]; */ + /* char *dot = rindex(file, '.'); */ + /* *dot = 0x0; */ + /* snprintf(icon,255,"%s/%s.png",dir,file); */ + /* FILE *icon_fd = fopen(icon,"r"); */ + /* if(icon_fd) { */ + /* fprintf(stderr," icon found: %s\n",icon); */ + /* } */ + + // get info about params + if(pi.num_params>0) { + fprintf(stdout,",\n \"params\":[\n"); + for(c=0; cc+1) { + fprintf(stdout,",\n"); + } else { + fprintf(stdout,"\n"); + } + } + fprintf(stdout," ]\n"); + } + fprintf(stdout,"\n}\n"); + fflush(stdout); + f0r_deinit(); + dlclose(dl_handle); + exit(0); +} diff --git a/test/frei0r-run.c b/test/frei0r-run.c new file mode 100644 index 0000000..6cc3254 --- /dev/null +++ b/test/frei0r-run.c @@ -0,0 +1,523 @@ +/* This file is part of frei0r (https://frei0r.dyne.org) + * + * Copyright (C) 2024-2025 Dyne.org foundation + * designed, written and maintained by Denis Roio + * + * 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 . + * + */ + +#include +#include +#include +#include +#include +#include +#include + +#include +#include "test-pattern.h" + +#if defined(__linux__) && defined(GUI) +#pragma message "Compiling GUI test" +#include +#include +#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 \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; +} diff --git a/test/test-pattern.c b/test/test-pattern.c new file mode 100644 index 0000000..41b71e7 --- /dev/null +++ b/test/test-pattern.c @@ -0,0 +1,243 @@ +/* This file is part of frei0r (https://frei0r.dyne.org) + * + * Copyright (C) 2024-2025 Dyne.org foundation + * + * 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 . + * + */ + +#include "test-pattern.h" +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +static void set_pixel(uint32_t* frame, int width, int height, int x, int y, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + if (x >= 0 && x < width && y >= 0 && y < height) { + if (color_model == 0) { + frame[y * width + x] = (a << 24) | (r << 16) | (g << 8) | b; + } else { + frame[y * width + x] = (a << 24) | (b << 16) | (g << 8) | r; + } + } +} + +static void draw_circle(uint32_t* frame, int width, int height, + int cx, int cy, int radius, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + int x_start = cx - radius > 0 ? cx - radius : 0; + int x_end = cx + radius < width ? cx + radius : width - 1; + int y_start = cy - radius > 0 ? cy - radius : 0; + int y_end = cy + radius < height ? cy + radius : height - 1; + + for (int y = y_start; y <= y_end; y++) { + for (int x = x_start; x <= x_end; x++) { + int dx = x - cx; + int dy = y - cy; + if (dx * dx + dy * dy <= radius * radius) { + set_pixel(frame, width, height, x, y, r, g, b, a, color_model); + } + } + } +} + +static void draw_filled_rect(uint32_t* frame, int width, int height, + int x1, int y1, int x2, int y2, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + if (x1 > x2) { int tmp = x1; x1 = x2; x2 = tmp; } + if (y1 > y2) { int tmp = y1; y1 = y2; y2 = tmp; } + + if (x1 < 0) x1 = 0; + if (y1 < 0) y1 = 0; + if (x2 >= width) x2 = width - 1; + if (y2 >= height) y2 = height - 1; + + for (int y = y1; y <= y2; y++) { + for (int x = x1; x <= x2; x++) { + set_pixel(frame, width, height, x, y, r, g, b, a, color_model); + } + } +} + +static void draw_rotated_square(uint32_t* frame, int width, int height, + int cx, int cy, int size, double angle, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + double cos_a = cos(angle); + double sin_a = sin(angle); + int half = size / 2; + + for (int dy = -half; dy <= half; dy++) { + for (int dx = -half; dx <= half; dx++) { + int rx = (int)(dx * cos_a - dy * sin_a); + int ry = (int)(dx * sin_a + dy * cos_a); + set_pixel(frame, width, height, cx + rx, cy + ry, r, g, b, a, color_model); + } + } +} + +static void draw_digit(uint32_t* frame, int width, int height, + int x, int y, int digit, int scale, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + // Simple 5x7 bitmap font for digits 0-9 + static const uint8_t font[10][7] = { + {0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F}, // 0 + {0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E}, // 1 + {0x1F, 0x01, 0x01, 0x1F, 0x10, 0x10, 0x1F}, // 2 + {0x1F, 0x01, 0x01, 0x1F, 0x01, 0x01, 0x1F}, // 3 + {0x11, 0x11, 0x11, 0x1F, 0x01, 0x01, 0x01}, // 4 + {0x1F, 0x10, 0x10, 0x1F, 0x01, 0x01, 0x1F}, // 5 + {0x1F, 0x10, 0x10, 0x1F, 0x11, 0x11, 0x1F}, // 6 + {0x1F, 0x01, 0x01, 0x02, 0x04, 0x08, 0x10}, // 7 + {0x1F, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x1F}, // 8 + {0x1F, 0x11, 0x11, 0x1F, 0x01, 0x01, 0x1F} // 9 + }; + + if (digit < 0 || digit > 9) return; + + for (int row = 0; row < 7; row++) { + uint8_t line = font[digit][row]; + for (int col = 0; col < 5; col++) { + if (line & (1 << (4 - col))) { + for (int sy = 0; sy < scale; sy++) { + for (int sx = 0; sx < scale; sx++) { + set_pixel(frame, width, height, + x + col * scale + sx, + y + row * scale + sy, + r, g, b, a, color_model); + } + } + } + } + } +} + +static void draw_number(uint32_t* frame, int width, int height, + int x, int y, int number, int scale, + uint8_t r, uint8_t g, uint8_t b, uint8_t a, int color_model) { + if (number == 0) { + draw_digit(frame, width, height, x, y, 0, scale, r, g, b, a, color_model); + return; + } + + // Count digits + int temp = number; + int digits = 0; + while (temp > 0) { + digits++; + temp /= 10; + } + + // Draw each digit + temp = number; + for (int i = digits - 1; i >= 0; i--) { + int digit = temp % 10; + draw_digit(frame, width, height, x + i * 6 * scale, y, digit, scale, r, g, b, a, color_model); + temp /= 10; + } +} + +static void draw_zone_plate_patch(uint32_t* frame, int width, int height, + int cx, int cy, int size, int frame_num, int color_model) { + double phase = frame_num * 0.05; + int half = size / 2; + + for (int dy = -half; dy < half; dy++) { + for (int dx = -half; dx < half; dx++) { + double dist = sqrt(dx * dx + dy * dy); + double freq = dist * 0.3; + double value = (sin(freq + phase) + 1.0) * 0.5; + + uint8_t intensity = (uint8_t)(value * 255); + set_pixel(frame, width, height, cx + dx, cy + dy, + intensity, intensity, intensity, 255, color_model); + } + } +} + +void generate_animated_test_pattern(uint32_t* frame, int width, int height, int frame_num, int color_model) { + // 1. Animated radial gradient background + int center_x = width / 2; + int center_y = height / 2; + double max_dist = sqrt(center_x * center_x + center_y * center_y); + + // Color cycling through the animation + double color_phase = frame_num * 0.02; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int dx = x - center_x; + int dy = y - center_y; + double dist = sqrt(dx * dx + dy * dy); + double norm_dist = dist / max_dist; + + // Create a pulsing radial gradient with color shift + double angle = atan2(dy, dx); + double r_val = (sin(norm_dist * M_PI + color_phase) + 1.0) * 0.5; + double g_val = (sin(norm_dist * M_PI + color_phase + M_PI * 2.0 / 3.0) + 1.0) * 0.5; + double b_val = (sin(norm_dist * M_PI + color_phase + M_PI * 4.0 / 3.0) + 1.0) * 0.5; + + // Add some angular variation + double angular_mod = (sin(angle * 3.0 + color_phase * 0.5) + 1.0) * 0.2; + + uint8_t r = (uint8_t)((r_val + angular_mod) * 127 + 32); + uint8_t g = (uint8_t)((g_val + angular_mod) * 127 + 32); + uint8_t b = (uint8_t)((b_val + angular_mod) * 127 + 32); + + if (color_model == 0) { + frame[y * width + x] = (255 << 24) | (r << 16) | (g << 8) | b; + } else { + frame[y * width + x] = (255 << 24) | (b << 16) | (g << 8) | r; + } + } + } + + // 2. Bouncing circle + int circle_radius = 40; + int circle_x = (int)(center_x + sin(frame_num * 0.05) * (center_x - circle_radius - 20)); + int circle_y = (int)(center_y + cos(frame_num * 0.037) * (center_y - circle_radius - 20)); + + draw_circle(frame, width, height, circle_x, circle_y, circle_radius, + 255, 200, 0, 255, color_model); + + // 3. Rotating square + int square_size = 60; + int square_x = width / 4; + int square_y = height / 4; + double rotation = frame_num * 0.03; + + draw_rotated_square(frame, width, height, square_x, square_y, square_size, rotation, + 0, 255, 200, 255, color_model); + + // 4. Horizontal moving bar + int bar_height = 20; + int bar_y = (int)(height * 0.75 + sin(frame_num * 0.04) * (height * 0.15)); + + draw_filled_rect(frame, width, height, 0, bar_y - bar_height/2, + width - 1, bar_y + bar_height/2, + 255, 100, 200, 255, color_model); + + // 5. Zone plate patch in corner + int zone_size = 80; + draw_zone_plate_patch(frame, width, height, + width - zone_size/2 - 10, + zone_size/2 + 10, + zone_size, frame_num, color_model); + + // 6. Frame counter + draw_number(frame, width, height, 10, 10, frame_num, 2, + 255, 255, 255, 255, color_model); +} diff --git a/test/test-pattern.h b/test/test-pattern.h new file mode 100644 index 0000000..484bb1d --- /dev/null +++ b/test/test-pattern.h @@ -0,0 +1,44 @@ +/* This file is part of frei0r (https://frei0r.dyne.org) + * + * Copyright (C) 2024-2025 Dyne.org foundation + * + * 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 . + * + */ + +#ifndef TEST_PATTERN_H +#define TEST_PATTERN_H + +#include + +/** + * Generate an animated test pattern for video filter testing + * + * This creates a comprehensive test pattern that includes: + * - Animated radial gradient background (tests color interpolation and smooth transitions) + * - Bouncing circle (tests circular features and motion) + * - Rotating square (tests corners, rotation, and sharp edges) + * - Moving horizontal bar (tests horizontal motion and rectangular shapes) + * - Zone plate patch (tests spatial frequency response and aliasing) + * - Frame counter (tests text/detail preservation) + * + * @param frame Output buffer (32-bit per pixel) + * @param width Frame width in pixels + * @param height Frame height in pixels + * @param frame_num Current frame number (for animation) + * @param color_model Color model (F0R_COLOR_MODEL_BGRA8888, F0R_COLOR_MODEL_RGBA8888, or F0R_COLOR_MODEL_PACKED32) + */ +void generate_animated_test_pattern(uint32_t* frame, int width, int height, int frame_num, int color_model); + +#endif // TEST_PATTERN_H