diff --git a/.github/workflows/build-distros.yml b/.github/workflows/build-distros.yml index 167e75969..cc367402e 100644 --- a/.github/workflows/build-distros.yml +++ b/.github/workflows/build-distros.yml @@ -34,27 +34,28 @@ jobs: sed -i 's/^Types: deb/Types: deb deb-src/' /etc/apt/sources.list.d/ubuntu.sources DEBIAN_FRONTEND=noninteractive apt-get -qq update DEBIAN_FRONTEND=noninteractive apt-get -yqq build-dep mlt - DEBIAN_FRONTEND=noninteractive apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev + DEBIAN_FRONTEND=noninteractive apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev libplacebo-dev - name: ubuntu-22.04 image: ubuntu:22.04 setup_script: | sed -i '/^#\sdeb-src /s/^#//' "/etc/apt/sources.list" DEBIAN_FRONTEND=noninteractive apt-get -qq update DEBIAN_FRONTEND=noninteractive apt-get -yqq build-dep mlt - DEBIAN_FRONTEND=noninteractive apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev libqt6core5compat6-dev + DEBIAN_FRONTEND=noninteractive apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev libqt6core5compat6-dev libplacebo-dev - name: debian-unstable image: debian:unstable setup_script: | echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list apt-get -qq update apt-get -yqq build-dep mlt + apt-get -yqq install libplacebo-dev - name: debian-testing image: debian:testing setup_script: | echo 'deb-src http://deb.debian.org/debian testing main' >> /etc/apt/sources.list apt-get -qq update apt-get -yqq build-dep mlt - apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev + apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev libplacebo-dev - name: debian-stable image: debian:stable setup_script: | @@ -62,7 +63,7 @@ jobs: echo 'deb http://deb.debian.org/debian bookworm-backports main' >> /etc/apt/sources.list apt-get -qq update apt-get -yqq build-dep mlt - apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev + apt-get -yqq install cmake qt6-base-dev libqt6svg6-dev libplacebo-dev - name: fedora-42 image: fedora:42 setup_script: | @@ -75,7 +76,8 @@ jobs: libtheora-devel libvorbis-devel libvdpau-devel \ libsoup-devel liboil-devel python-devel alsa-lib \ pulseaudio-libs-devel gcc-c++ cmake ffmpeg-free-devel \ - movit-devel rubberband-devel vid.stab-devel + movit-devel rubberband-devel vid.stab-devel \ + libplacebo-devel - name: fedora-38 image: fedora:38 setup_script: | diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 0e618634f..892ff4165 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -39,7 +39,7 @@ jobs: sudo apt-get -qq update sudo apt-get -yqq build-dep mlt sudo apt-get -yqq install qt6-base-dev libqt6svg6-dev libqt6core5compat6-dev - sudo apt-get -yqq install cmake ninja-build kwalify + sudo apt-get -yqq install cmake ninja-build kwalify libplacebo-dev cmake -D CMAKE_BUILD_TYPE=Debug -D BUILD_TESTING=ON -D SWIG_PYTHON=ON -S . -B build -G Ninja cmake --build build sudo cmake --install build diff --git a/.github/workflows/build-msys2-mingw64.yml b/.github/workflows/build-msys2-mingw64.yml index 2417926cd..28dca5893 100644 --- a/.github/workflows/build-msys2-mingw64.yml +++ b/.github/workflows/build-msys2-mingw64.yml @@ -59,6 +59,7 @@ jobs: mingw-w64-x86_64-x264 mingw-w64-x86_64-x265 mingw-w64-x86_64-zimg + mingw-w64-x86_64-libplacebo - uses: actions/checkout@v4 diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml index 00a95de14..f1de06aaf 100644 --- a/.github/workflows/static-code-analysis.yml +++ b/.github/workflows/static-code-analysis.yml @@ -19,6 +19,9 @@ jobs: --library=cppcheck.cfg --suppress=ctuOneDefinitionRuleViolation --suppress=syntaxError:src/modules/xml/common.c + --suppress=syntaxError:src/modules/placebo/filter_placebo_render.c + --suppress=syntaxError:src/modules/placebo/filter_placebo_shader.c + --suppress=syntaxError:src/modules/placebo/gpu_context.c steps: - uses: actions/checkout@v4 - name: Install Cppcheck diff --git a/CMakeLists.txt b/CMakeLists.txt index 1540c5ee3..ff7cfd1f4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,7 @@ option(MOD_NORMALIZE "Enable Normalize module (GPL)" ON) option(MOD_OLDFILM "Enable Oldfilm module" ON) option(MOD_OPENCV "Enable OpenCV module" OFF) option(MOD_MOVIT "Enable OpenGL module" ON) +option(MOD_PLACEBO "Enable libplacebo GPU module" ON) option(MOD_PLUS "Enable Plus module" ON) option(MOD_PLUSGPL "Enable PlusGPL module (GPL)" ON) option(MOD_QT6 "Enable Qt6 module (GPL)" ON) @@ -303,6 +304,15 @@ if(MOD_MOVIT) list(APPEND MLT_SUPPORTED_COMPONENTS movit) endif() +if(MOD_PLACEBO) + pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo) + if(libplacebo_FOUND) + list(APPEND MLT_SUPPORTED_COMPONENTS placebo) + else() + set(MOD_PLACEBO OFF) + endif() +endif() + if(MOD_PLUS) pkg_check_modules(libebur128 IMPORTED_TARGET libebur128) list(APPEND MLT_SUPPORTED_COMPONENTS plus) @@ -568,6 +578,7 @@ add_feature_info("Module: Normalize" MOD_NORMALIZE "") add_feature_info("Module: Oldfilm" MOD_OLDFILM "") add_feature_info("Module: OpenCV" MOD_OPENCV "") add_feature_info("Module: Movit" MOD_MOVIT "") +add_feature_info("Module: Placebo" MOD_PLACEBO "") add_feature_info("Module: Plus" MOD_PLUS "") add_feature_info("Module: PlusGPL" MOD_PLUSGPL "") add_feature_info("Module: Qt6" MOD_QT6 "") diff --git a/src/modules/CMakeLists.txt b/src/modules/CMakeLists.txt index bdc46c888..824b0f7a5 100644 --- a/src/modules/CMakeLists.txt +++ b/src/modules/CMakeLists.txt @@ -48,6 +48,10 @@ if(MOD_MOVIT) add_subdirectory(movit) endif() +if(MOD_PLACEBO) + add_subdirectory(placebo) +endif() + if(MOD_PLUS) add_subdirectory(plus) endif() diff --git a/src/modules/placebo/CMakeLists.txt b/src/modules/placebo/CMakeLists.txt new file mode 100644 index 000000000..92775505c --- /dev/null +++ b/src/modules/placebo/CMakeLists.txt @@ -0,0 +1,55 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo>=5.229) +if(NOT libplacebo_FOUND) + return() +endif() + +add_library(mltplacebo MODULE + factory.c + gpu_context.c gpu_context.h + filter_placebo_render.c + filter_placebo_shader.c +) + +file(GLOB YML "*.yml") +add_custom_target(Other_placebo_Files SOURCES + ${YML} +) + +include(GenerateExportHeader) +generate_export_header(mltplacebo) +target_compile_options(mltplacebo PRIVATE ${MLT_COMPILE_OPTIONS}) +target_include_directories(mltplacebo PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(mltplacebo PRIVATE mlt PkgConfig::libplacebo) + +# Only link D3D11 if libplacebo was built with D3D11 support +include(CheckCSourceCompiles) +set(CMAKE_REQUIRED_INCLUDES ${libplacebo_INCLUDE_DIRS}) +check_c_source_compiles(" + #include + #if !defined(PL_HAVE_D3D11) || !PL_HAVE_D3D11 + #error no d3d11 + #endif + int main(void) { return 0; } +" PLACEBO_HAS_D3D11) +if(PLACEBO_HAS_D3D11) + target_link_libraries(mltplacebo PRIVATE d3d11 dxgi) +endif() +if(WIN32) + target_link_libraries(mltplacebo PRIVATE shell32 ole32) +endif() + +if(MSVC) + target_link_libraries(mltplacebo PRIVATE PThreads4W::PThreads4W) +endif() + +set_target_properties(mltplacebo PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${MLT_MODULE_OUTPUT_DIRECTORY}") + +install(TARGETS mltplacebo LIBRARY DESTINATION ${MLT_INSTALL_MODULE_DIR}) + +install(FILES + filter_placebo_render.yml + filter_placebo_shader.yml + DESTINATION ${MLT_INSTALL_DATA_DIR}/placebo +) diff --git a/src/modules/placebo/factory.c b/src/modules/placebo/factory.c new file mode 100644 index 000000000..413d73480 --- /dev/null +++ b/src/modules/placebo/factory.c @@ -0,0 +1,53 @@ +/* + * factory.c -- module registration for libplacebo filters + * Copyright (C) 2025 D-Ogi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "mltplacebo_export.h" +#include +#include +#include + +extern mlt_filter filter_placebo_render_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg); +extern mlt_filter filter_placebo_shader_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg); + +static mlt_properties metadata(mlt_service_type type, const char *id, void *data) +{ + char file[PATH_MAX]; + snprintf(file, PATH_MAX, "%s/placebo/%s", mlt_environment("MLT_DATA"), (char *) data); + return mlt_properties_parse_yaml(file); +} + +MLTPLACEBO_EXPORT MLT_REPOSITORY +{ + MLT_REGISTER(mlt_service_filter_type, "placebo.render", filter_placebo_render_init); + MLT_REGISTER(mlt_service_filter_type, "placebo.shader", filter_placebo_shader_init); + MLT_REGISTER_METADATA(mlt_service_filter_type, + "placebo.render", + metadata, + "filter_placebo_render.yml"); + MLT_REGISTER_METADATA(mlt_service_filter_type, + "placebo.shader", + metadata, + "filter_placebo_shader.yml"); +} diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c new file mode 100644 index 000000000..49f754a08 --- /dev/null +++ b/src/modules/placebo/filter_placebo_render.c @@ -0,0 +1,286 @@ +/* + * filter_placebo_render.c -- GPU-accelerated renderer via libplacebo + * Copyright (C) 2025 D-Ogi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "gpu_context.h" + +#include +#include +#include + +#include +#include +/* pl_deband_params, pl_dither_params, pl_color_map_params all come from renderer.h */ + +#include +#include + +/* Map upscaler/downscaler name to pl_filter_config */ +static const struct pl_filter_config *lookup_scaler(const char *name) +{ + if (!name || !*name) + return &pl_filter_ewa_lanczos; + if (!strcmp(name, "bilinear")) + return &pl_filter_bilinear; + if (!strcmp(name, "catmull_rom")) + return &pl_filter_catmull_rom; + if (!strcmp(name, "mitchell")) + return &pl_filter_mitchell; + if (!strcmp(name, "lanczos")) + return &pl_filter_lanczos; + if (!strcmp(name, "ewa_lanczos")) + return &pl_filter_ewa_lanczos; + if (!strcmp(name, "spline36")) + return &pl_filter_spline36; + return &pl_filter_ewa_lanczos; +} + +/* Map dithering name to pl_dither_method */ +static enum pl_dither_method lookup_dither(const char *name) +{ + if (!name || !*name || !strcmp(name, "blue")) + return PL_DITHER_BLUE_NOISE; + if (!strcmp(name, "ordered_lut")) + return PL_DITHER_ORDERED_LUT; + if (!strcmp(name, "white")) + return PL_DITHER_WHITE_NOISE; + return PL_DITHER_BLUE_NOISE; +} + +/* Map tonemapping name to pl_tone_map_function */ +static const struct pl_tone_map_function *lookup_tonemap(const char *name) +{ + if (!name || !*name || !strcmp(name, "auto")) + return &pl_tone_map_auto; + if (!strcmp(name, "clip")) + return &pl_tone_map_clip; + if (!strcmp(name, "mobius")) + return &pl_tone_map_mobius; + if (!strcmp(name, "reinhard")) + return &pl_tone_map_reinhard; + if (!strcmp(name, "hable")) + return &pl_tone_map_hable; + if (!strcmp(name, "bt.2390") || !strcmp(name, "bt2390")) + return &pl_tone_map_bt2390; + if (!strcmp(name, "spline")) + return &pl_tone_map_spline; + return &pl_tone_map_auto; +} + +static int filter_get_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int *width, + int *height, + int writable) +{ + mlt_filter filter = mlt_frame_pop_service(frame); + mlt_properties filter_props = MLT_FILTER_PROPERTIES(filter); + + pl_gpu gpu = placebo_gpu_get(); + pl_renderer renderer = placebo_renderer_get(); + if (!gpu || !renderer) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU not available, passing frame through\n"); + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + + /* Not all GPU backends guarantee rgba8 support (e.g. some OpenGL ES) */ + pl_fmt rgba_fmt = pl_find_named_fmt(gpu, "rgba8"); + if (!rgba_fmt) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "GPU does not support rgba8 format\n"); + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + + /* Request RGBA from upstream */ + *format = mlt_image_rgba; + int error = mlt_frame_get_image(frame, image, format, width, height, 1); + if (error || !*image) + return error; + + int w = *width; + int h = *height; + size_t stride = w * 4; + + /* pl_renderer is not thread-safe; hold the lock for the entire + * upload → render → download sequence */ + placebo_render_lock(); + + /* Try to reuse a GPU texture left by a preceding placebo filter on + * this frame. Returns NULL if there is none, or if the RAM buffer was + * reallocated by an intervening CPU filter (stale texture). */ + pl_tex reused_src = placebo_frame_take_tex(frame, *image); + pl_tex src_tex; + if (reused_src) { + src_tex = reused_src; + } else { + src_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .sampleable = true, + .host_writable = true, )); + if (!src_tex) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create source texture\n"); + placebo_render_unlock(); + return 0; + } + + if (!pl_tex_upload(gpu, + pl_tex_transfer_params(.tex = src_tex, + .row_pitch = stride, + .ptr = *image, ))) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "GPU texture upload failed\n"); + pl_tex_destroy(gpu, &src_tex); + placebo_render_unlock(); + return 0; + } + } + + /* sampleable is needed so a subsequent placebo filter can bind this + * texture as its source without re-uploading from RAM. */ + pl_tex dst_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .sampleable = true, + .renderable = true, + .host_readable = true, )); + if (!dst_tex) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create dest texture\n"); + pl_tex_destroy(gpu, &src_tex); + placebo_render_unlock(); + return 0; + } + + /* Build source and target pl_frames */ + struct pl_frame pl_src = { + .num_planes = 1, + .planes = {{ + .texture = src_tex, + .components = 4, + .component_mapping = {0, 1, 2, 3}, + }}, + .repr = pl_color_repr_rgb, + .color = pl_color_space_srgb, + }; + + struct pl_frame pl_dst = { + .num_planes = 1, + .planes = {{ + .texture = dst_tex, + .components = 4, + .component_mapping = {0, 1, 2, 3}, + }}, + .repr = pl_color_repr_rgb, + .color = pl_color_space_srgb, + }; + + /* Configure render params from filter properties */ + const char *preset = mlt_properties_get(filter_props, "preset"); + struct pl_render_params params; + if (preset && !strcmp(preset, "high_quality")) + params = pl_render_high_quality_params; + else if (preset && !strcmp(preset, "fast")) + params = pl_render_fast_params; + else + params = pl_render_default_params; + + /* Scalers */ + const char *upscaler = mlt_properties_get(filter_props, "upscaler"); + const char *downscaler = mlt_properties_get(filter_props, "downscaler"); + params.upscaler = lookup_scaler(upscaler); + params.downscaler = lookup_scaler(downscaler); + + /* Debanding */ + struct pl_deband_params deband_params = pl_deband_default_params; + if (mlt_properties_get_int(filter_props, "deband")) { + int iterations = mlt_properties_get_int(filter_props, "deband_iterations"); + if (iterations > 0) + deband_params.iterations = iterations; + params.deband_params = &deband_params; + } else { + params.deband_params = NULL; + } + + /* Dithering */ + const char *dither_name = mlt_properties_get(filter_props, "dithering"); + struct pl_dither_params dither_params = pl_dither_default_params; + if (dither_name && !strcmp(dither_name, "none")) { + params.dither_params = NULL; + } else { + dither_params.method = lookup_dither(dither_name); + params.dither_params = &dither_params; + } + + /* Tone mapping */ + const char *tonemap_name = mlt_properties_get(filter_props, "tonemapping"); + struct pl_color_map_params color_map_params = pl_color_map_default_params; + color_map_params.tone_mapping_function = lookup_tonemap(tonemap_name); + params.color_map_params = &color_map_params; + + /* Render */ + if (!pl_render_image(renderer, &pl_src, &pl_dst, ¶ms)) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "pl_render_image failed\n"); + } + + /* Always download to RAM — MLT expects *image to hold current pixels, + * even though the texture may be reused on the GPU side. */ + if (!pl_tex_download(gpu, + pl_tex_transfer_params(.tex = dst_tex, + .row_pitch = stride, + .ptr = *image, ))) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU texture download failed\n"); + } + + pl_tex_destroy(gpu, &src_tex); + /* Keep dst_tex alive on the frame for the next placebo filter to + * pick up via take_tex(). Unclaimed textures are freed automatically + * by the frame destructor (frame_gpu_destroy). */ + placebo_frame_put_tex(frame, dst_tex, *image); + + placebo_render_unlock(); + + return 0; +} + +static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) +{ + mlt_frame_push_service(frame, filter); + mlt_frame_push_get_image(frame, filter_get_image); + return frame; +} + +mlt_filter filter_placebo_render_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg) +{ + mlt_filter filter = mlt_filter_new(); + if (filter) { + filter->process = filter_process; + mlt_properties props = MLT_FILTER_PROPERTIES(filter); + mlt_properties_set(props, "preset", "default"); + mlt_properties_set(props, "upscaler", "ewa_lanczos"); + mlt_properties_set(props, "downscaler", "mitchell"); + mlt_properties_set_int(props, "deband", 0); + mlt_properties_set_int(props, "deband_iterations", 1); + mlt_properties_set(props, "dithering", "blue"); + mlt_properties_set(props, "tonemapping", "auto"); + } + return filter; +} diff --git a/src/modules/placebo/filter_placebo_render.yml b/src/modules/placebo/filter_placebo_render.yml new file mode 100644 index 000000000..2420a4e3c --- /dev/null +++ b/src/modules/placebo/filter_placebo_render.yml @@ -0,0 +1,89 @@ +schema_version: 7.0 +type: filter +identifier: placebo.render +title: GPU Render (libplacebo) +version: 1 +copyright: Copyright (C) 2025 D-Ogi +creator: D-Ogi +license: LGPLv2.1 +language: en +tags: + - Video +description: > + GPU-accelerated scaling, debanding, dithering and tonemapping via libplacebo. + Uses D3D11 on Windows, Vulkan elsewhere. Processes every frame through + pl_renderer with configurable quality presets and scaling algorithms. + +parameters: + - identifier: preset + title: Quality Preset + type: string + description: > + Overall quality preset. "fast" minimizes GPU work, "default" is balanced, + "high_quality" enables all enhancements. + default: default + mutable: yes + readonly: no + widget: combo + + - identifier: upscaler + title: Upscaler + type: string + description: > + Scaling algorithm for upscaling. ewa_lanczos (default) gives the best + quality; bilinear is fastest. + default: ewa_lanczos + mutable: yes + readonly: no + widget: combo + + - identifier: downscaler + title: Downscaler + type: string + description: > + Scaling algorithm for downscaling. + default: mitchell + mutable: yes + readonly: no + widget: combo + + - identifier: deband + title: Debanding + type: integer + description: Enable debanding to reduce color banding artifacts. + minimum: 0 + maximum: 1 + default: 0 + mutable: yes + widget: checkbox + + - identifier: deband_iterations + title: Deband Iterations + type: integer + description: Number of debanding iterations (1-4). Higher values are slower but more effective. + minimum: 1 + maximum: 4 + default: 1 + mutable: yes + + - identifier: dithering + title: Dithering + type: string + description: > + Dithering method. "blue" (default) looks best, "ordered_lut" is fastest, + "none" disables dithering. + default: blue + mutable: yes + readonly: no + widget: combo + + - identifier: tonemapping + title: Tone Mapping + type: string + description: > + Tone mapping function for HDR-to-SDR conversion. + "auto" selects the best method automatically. + default: auto + mutable: yes + readonly: no + widget: combo diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c new file mode 100644 index 000000000..bcc7e3063 --- /dev/null +++ b/src/modules/placebo/filter_placebo_shader.c @@ -0,0 +1,497 @@ +/* + * filter_placebo_shader.c -- custom .hook shader loader via libplacebo + * Copyright (C) 2025 D-Ogi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "gpu_context.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +/* MSVC marks stat() as deprecated; _stat is the replacement */ +#include +#define stat_func _stat +#define stat_struct _stat +#else +#include +#define stat_func stat +#define stat_struct stat +#endif + +/* ---- Base64 decoder (RFC 4648) ---- */ +static const unsigned char b64_table[256] = { + ['A'] = 0, ['B'] = 1, ['C'] = 2, ['D'] = 3, ['E'] = 4, ['F'] = 5, ['G'] = 6, ['H'] = 7, + ['I'] = 8, ['J'] = 9, ['K'] = 10, ['L'] = 11, ['M'] = 12, ['N'] = 13, ['O'] = 14, ['P'] = 15, + ['Q'] = 16, ['R'] = 17, ['S'] = 18, ['T'] = 19, ['U'] = 20, ['V'] = 21, ['W'] = 22, ['X'] = 23, + ['Y'] = 24, ['Z'] = 25, ['a'] = 26, ['b'] = 27, ['c'] = 28, ['d'] = 29, ['e'] = 30, ['f'] = 31, + ['g'] = 32, ['h'] = 33, ['i'] = 34, ['j'] = 35, ['k'] = 36, ['l'] = 37, ['m'] = 38, ['n'] = 39, + ['o'] = 40, ['p'] = 41, ['q'] = 42, ['r'] = 43, ['s'] = 44, ['t'] = 45, ['u'] = 46, ['v'] = 47, + ['w'] = 48, ['x'] = 49, ['y'] = 50, ['z'] = 51, ['0'] = 52, ['1'] = 53, ['2'] = 54, ['3'] = 55, + ['4'] = 56, ['5'] = 57, ['6'] = 58, ['7'] = 59, ['8'] = 60, ['9'] = 61, ['+'] = 62, ['/'] = 63, +}; + +/* Decode base64 in-place. Returns decoded length, or -1 on error. */ +static long b64_decode(const char *src, size_t src_len, char *dst) +{ + size_t i = 0; + long out = 0; + while (i < src_len) { + /* skip whitespace / padding */ + if (src[i] == '=' || src[i] == '\n' || src[i] == '\r' || src[i] == ' ') { + i++; + continue; + } + if (i + 1 >= src_len) + break; + unsigned int a = b64_table[(unsigned char) src[i]]; + unsigned int b = b64_table[(unsigned char) src[i + 1]]; + unsigned int c = (i + 2 < src_len && src[i + 2] != '=') + ? b64_table[(unsigned char) src[i + 2]] + : 0; + unsigned int d = (i + 3 < src_len && src[i + 3] != '=') + ? b64_table[(unsigned char) src[i + 3]] + : 0; + dst[out++] = (char) ((a << 2) | (b >> 4)); + if (i + 2 < src_len && src[i + 2] != '=') + dst[out++] = (char) (((b & 0xF) << 4) | (c >> 2)); + if (i + 3 < src_len && src[i + 3] != '=') + dst[out++] = (char) (((c & 0x3) << 6) | d); + i += 4; + } + dst[out] = '\0'; + return out; +} + +/* Private data stored on the filter */ +typedef struct +{ + const struct pl_hook *hooks[1]; /* parsed shader hook (single) */ + int num_hooks; + char *loaded_path; /* path that was loaded */ + time_t loaded_mtime; /* mtime when last loaded */ + char *loaded_text; /* inline text that was loaded */ +} shader_private; + +/* Destructor registered via mlt_properties_set_data; handles both normal + * filter teardown and early destruction if the filter is removed mid-session. */ +static void shader_private_destroy(void *ptr) +{ + shader_private *priv = (shader_private *) ptr; + if (!priv) + return; + if (priv->hooks[0]) + pl_mpv_user_shader_destroy(&priv->hooks[0]); + priv->num_hooks = 0; + free(priv->loaded_path); + priv->loaded_path = NULL; + free(priv->loaded_text); + priv->loaded_text = NULL; + free(priv); +} + +/* Read entire file into malloc'd string; caller must free. */ +static char *read_file(const char *path, size_t *out_len) +{ + FILE *f = fopen(path, "rb"); + if (!f) + return NULL; + + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + + if (len <= 0 || len > 10 * 1024 * 1024) { /* sanity limit for shader files */ + fclose(f); + return NULL; + } + + char *buf = malloc(len + 1); + if (!buf) { + fclose(f); + return NULL; + } + if ((long) fread(buf, 1, len, f) != len) { + free(buf); + fclose(f); + return NULL; + } + buf[len] = '\0'; + fclose(f); + + if (out_len) + *out_len = (size_t) len; + return buf; +} + +static time_t file_mtime(const char *path) +{ + struct stat_struct st; + if (stat_func(path, &st) == 0) + return st.st_mtime; + return 0; +} + +/* ---------- Shader parameter override ---------- + * + * libplacebo's pl_hook exposes tunable DYNAMIC parameters (declared via + * //!PARAM + //!TYPE DYNAMIC in the shader source). After parsing, each + * parameter lives in hook->parameters[] with a mutable data pointer + * (par->data->f / .i / .u) that can be written before every render call. + * + * The NLE (Kdenlive) stores user values as MLT animated properties with + * the prefix "shader_param." — e.g. "shader_param.color_r". Animated + * properties use MLT's keyframe string format ("0=200;50=100"), so we + * must resolve them at the current frame position via the anim_get API. + * Using plain atof() would parse only the frame number before '=' and + * return the wrong value. + * + * Parameters that the user has not touched are absent from MLT properties; + * those keep the defaults baked into the //!PARAM block by libplacebo. */ +static void apply_shader_params(mlt_filter filter, + mlt_frame frame, + mlt_properties props, + const struct pl_hook *hook) +{ + if (!hook || hook->num_parameters <= 0) + return; + + mlt_position position = mlt_filter_get_position(filter, frame); + mlt_position length = mlt_filter_get_length2(filter, frame); + + int n = mlt_properties_count(props); + for (int i = 0; i < n; i++) { + const char *key = mlt_properties_get_name(props, i); + if (!key || strncmp(key, "shader_param.", 13) != 0) + continue; + + const char *param_name = key + 13; /* bare name after prefix */ + + /* Match against the hook's exported parameters by name */ + for (int j = 0; j < hook->num_parameters; j++) { + const struct pl_hook_par *par = &hook->parameters[j]; + if (strcmp(par->name, param_name) == 0 && par->data) { + /* par->data is explicitly mutable (pl_var_data *) even + * though the hook itself is const — libplacebo documents + * it as "may be updated at any time by the user". */ + if (par->type == PL_VAR_FLOAT) { + par->data->f + = (float) mlt_properties_anim_get_double(props, key, position, length); + } else if (par->type == PL_VAR_SINT) { + par->data->i = mlt_properties_anim_get_int(props, key, position, length); + } else if (par->type == PL_VAR_UINT) { + par->data->u + = (unsigned) mlt_properties_anim_get_int(props, key, position, length); + } + break; + } + } + } +} + +/* Load/reload shader if needed. Returns 1 if hooks are available. */ +static int ensure_shader(mlt_filter filter, shader_private *priv, pl_gpu gpu) +{ + mlt_properties props = MLT_FILTER_PROPERTIES(filter); + const char *shader_path = mlt_properties_get(props, "shader_path"); + const char *shader_text = mlt_properties_get(props, "shader_text"); + + /* Check if we need to reload from file */ + if (shader_path && *shader_path) { + time_t mtime = file_mtime(shader_path); + int need_reload = 0; + + if (!priv->loaded_path || strcmp(priv->loaded_path, shader_path) != 0) + need_reload = 1; + else if (mtime != priv->loaded_mtime) + need_reload = 1; + + if (need_reload) { + /* Free old shader */ + if (priv->hooks[0]) + pl_mpv_user_shader_destroy(&priv->hooks[0]); + priv->num_hooks = 0; + free(priv->loaded_path); + priv->loaded_path = NULL; + + /* Read and parse */ + size_t len = 0; + char *text = read_file(shader_path, &len); + if (!text) { + mlt_log_error(MLT_FILTER_SERVICE(filter), + "Failed to read shader file: %s\n", + shader_path); + return 0; + } + + priv->hooks[0] = pl_mpv_user_shader_parse(gpu, text, len); + free(text); + + if (!priv->hooks[0]) { + mlt_log_error(MLT_FILTER_SERVICE(filter), + "Failed to parse shader: %s\n", + shader_path); + return 0; + } + + priv->num_hooks = 1; + priv->loaded_path = strdup(shader_path); + priv->loaded_mtime = mtime; + mlt_log_info(MLT_FILTER_SERVICE(filter), "Loaded shader: %s\n", shader_path); + } + return priv->num_hooks > 0; + } + + /* Check inline shader_text */ + if (shader_text && *shader_text) { + if (!priv->loaded_text || strcmp(priv->loaded_text, shader_text) != 0) { + /* Free old shader */ + if (priv->hooks[0]) + pl_mpv_user_shader_destroy(&priv->hooks[0]); + priv->num_hooks = 0; + free(priv->loaded_text); + priv->loaded_text = NULL; + + /* Decode base64-encoded shader_text (prefix "base64:") */ + const char *parse_text = shader_text; + size_t parse_len = strlen(shader_text); + char *decoded = NULL; + if (strncmp(shader_text, "base64:", 7) == 0) { + const char *b64 = shader_text + 7; + size_t b64_len = strlen(b64); + decoded = malloc(b64_len + 1); /* decoded is always <= input */ + if (decoded) { + long dec_len = b64_decode(b64, b64_len, decoded); + if (dec_len > 0) { + parse_text = decoded; + parse_len = (size_t) dec_len; + } + } + } + + priv->hooks[0] = pl_mpv_user_shader_parse(gpu, parse_text, parse_len); + if (!priv->hooks[0]) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to parse inline shader_text\n"); + free(decoded); + return 0; + } + + priv->num_hooks = 1; + priv->loaded_text = strdup(shader_text); + mlt_log_info(MLT_FILTER_SERVICE(filter), + "Loaded inline shader (%" PRIu64 " bytes%s)\n", + (uint64_t) parse_len, + decoded ? ", base64-decoded" : ""); + free(decoded); + } + return priv->num_hooks > 0; + } + + return 0; /* no shader configured */ +} + +static int filter_get_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int *width, + int *height, + int writable) +{ + mlt_filter filter = mlt_frame_pop_service(frame); + mlt_properties filter_props = MLT_FILTER_PROPERTIES(filter); + + pl_gpu gpu = placebo_gpu_get(); + pl_renderer renderer = placebo_renderer_get(); + if (!gpu || !renderer) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU not available, passing frame through\n"); + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + + /* Not all GPU backends guarantee rgba8 support (e.g. some OpenGL ES) */ + pl_fmt rgba_fmt = pl_find_named_fmt(gpu, "rgba8"); + if (!rgba_fmt) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "GPU does not support rgba8 format\n"); + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + + /* Lazily allocate private data; destruction is handled by mlt_properties */ + shader_private *priv = mlt_properties_get_data(filter_props, "_shader_priv", NULL); + if (!priv) { + priv = calloc(1, sizeof(shader_private)); + if (!priv) + return mlt_frame_get_image(frame, image, format, width, height, writable); + mlt_properties_set_data(filter_props, "_shader_priv", priv, 0, shader_private_destroy, NULL); + } + + /* Ensure shader is loaded */ + if (!ensure_shader(filter, priv, gpu)) { + /* No shader available -- pass through */ + return mlt_frame_get_image(frame, image, format, width, height, writable); + } + + /* Request RGBA from upstream */ + *format = mlt_image_rgba; + int error = mlt_frame_get_image(frame, image, format, width, height, 1); + if (error || !*image) + return error; + + int w = *width; + int h = *height; + size_t stride = w * 4; + + /* pl_renderer is not thread-safe; hold the lock for the entire + * upload → render → download sequence */ + placebo_render_lock(); + + /* Try to reuse a GPU texture left by a preceding placebo filter on + * this frame. Returns NULL if there is none, or if the RAM buffer was + * reallocated by an intervening CPU filter (stale texture). */ + pl_tex reused_src = placebo_frame_take_tex(frame, *image); + pl_tex src_tex; + if (reused_src) { + src_tex = reused_src; + } else { + src_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .sampleable = true, + .host_writable = true, )); + if (!src_tex) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create source texture\n"); + placebo_render_unlock(); + return 0; + } + + if (!pl_tex_upload(gpu, + pl_tex_transfer_params(.tex = src_tex, + .row_pitch = stride, + .ptr = *image, ))) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "GPU texture upload failed\n"); + pl_tex_destroy(gpu, &src_tex); + placebo_render_unlock(); + return 0; + } + } + + /* sampleable is needed so a subsequent placebo filter can bind this + * texture as its source without re-uploading from RAM. */ + pl_tex dst_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .sampleable = true, + .renderable = true, + .host_readable = true, )); + if (!dst_tex) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create dest texture\n"); + pl_tex_destroy(gpu, &src_tex); + placebo_render_unlock(); + return 0; + } + + /* Build pl_frames */ + struct pl_frame pl_src = { + .num_planes = 1, + .planes = {{ + .texture = src_tex, + .components = 4, + .component_mapping = {0, 1, 2, 3}, + }}, + .repr = pl_color_repr_rgb, + .color = pl_color_space_srgb, + }; + + struct pl_frame pl_dst = { + .num_planes = 1, + .planes = {{ + .texture = dst_tex, + .components = 4, + .component_mapping = {0, 1, 2, 3}, + }}, + .repr = pl_color_repr_rgb, + .color = pl_color_space_srgb, + }; + + /* Override DYNAMIC shader parameters with values from MLT properties. + * Must happen after textures are set up but before pl_render_image, + * because the render call reads par->data to build the shader. */ + if (priv->num_hooks > 0 && priv->hooks[0]) { + apply_shader_params(filter, frame, filter_props, priv->hooks[0]); + } + + /* Render with shader hooks */ + struct pl_render_params params = pl_render_default_params; + params.hooks = priv->hooks; + params.num_hooks = priv->num_hooks; + + if (!pl_render_image(renderer, &pl_src, &pl_dst, ¶ms)) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "pl_render_image with shader hook failed\n"); + } + + /* Always download to RAM — MLT expects *image to hold current pixels, + * even though the texture may be reused on the GPU side. */ + if (!pl_tex_download(gpu, + pl_tex_transfer_params(.tex = dst_tex, + .row_pitch = stride, + .ptr = *image, ))) { + mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU texture download failed\n"); + } + + pl_tex_destroy(gpu, &src_tex); + /* Keep dst_tex alive on the frame for the next placebo filter to + * pick up via take_tex(). Unclaimed textures are freed automatically + * by the frame destructor (frame_gpu_destroy). */ + placebo_frame_put_tex(frame, dst_tex, *image); + + placebo_render_unlock(); + + return 0; +} + +static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) +{ + mlt_frame_push_service(frame, filter); + mlt_frame_push_get_image(frame, filter_get_image); + return frame; +} + +mlt_filter filter_placebo_shader_init(mlt_profile profile, + mlt_service_type type, + const char *id, + char *arg) +{ + mlt_filter filter = mlt_filter_new(); + if (filter) { + filter->process = filter_process; + mlt_properties props = MLT_FILTER_PROPERTIES(filter); + mlt_properties_set(props, "shader_path", arg ? arg : ""); + mlt_properties_set(props, "shader_text", ""); + } + return filter; +} diff --git a/src/modules/placebo/filter_placebo_shader.yml b/src/modules/placebo/filter_placebo_shader.yml new file mode 100644 index 000000000..22da5ae95 --- /dev/null +++ b/src/modules/placebo/filter_placebo_shader.yml @@ -0,0 +1,38 @@ +schema_version: 7.0 +type: filter +identifier: placebo.shader +title: GPU Shader (libplacebo) +version: 1 +copyright: Copyright (C) 2025 D-Ogi +creator: D-Ogi +license: LGPLv2.1 +language: en +tags: + - Video +description: > + Load and apply a custom mpv/libplacebo .hook shader file for GPU-accelerated + video processing. Supports Anime4K, FSRCNNX, film grain, and other + mpv-compatible user shaders. The shader file is hot-reloaded when modified. + +parameters: + - identifier: shader_path + title: Shader File + type: string + description: > + Absolute path to a .hook or .glsl shader file (mpv user shader format). + The file is monitored for changes and automatically reloaded. + argument: yes + default: "" + mutable: yes + readonly: no + widget: fileopen + + - identifier: shader_text + title: Shader Text + type: string + description: > + Inline shader source code as an alternative to shader_path. + If both are set, shader_path takes priority. + default: "" + mutable: yes + readonly: no diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c new file mode 100644 index 000000000..3c6b85b9e --- /dev/null +++ b/src/modules/placebo/gpu_context.c @@ -0,0 +1,515 @@ +/* + * gpu_context.c -- shared libplacebo GPU lifecycle (singleton) + * Copyright (C) 2025 D-Ogi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "gpu_context.h" + +#include +#include +#include +#include +#include +#include + +#ifdef PL_HAVE_D3D11 +#include +#endif + +#ifdef PL_HAVE_VULKAN +#include +#endif + +#ifdef PL_HAVE_OPENGL +#include +#endif + +#include + +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#else +#include +#include +#include +#endif + +/* ---------- Singleton state ---------- */ + +static pl_log s_log = NULL; + +#ifdef PL_HAVE_D3D11 +static pl_d3d11 s_d3d11 = NULL; +#endif + +#ifdef PL_HAVE_VULKAN +static pl_vulkan s_vulkan = NULL; +static pl_vk_inst s_vk_inst = NULL; /* must outlive pl_vulkan; destroyed in gpu_release */ +#ifdef _WIN32 +static HMODULE s_vulkan_dll = NULL; /* dynamically loaded vulkan-1.dll */ +#endif +#endif + +#ifdef PL_HAVE_OPENGL +static pl_opengl s_opengl = NULL; +#endif + +static pl_gpu s_gpu = NULL; +static pl_dispatch s_dispatch = NULL; +static pl_renderer s_renderer = NULL; +static pl_cache s_cache = NULL; + +static int s_initialized = 0; + +/* ---------- Mutex (SRWLOCK on Windows is statically initializable, unlike CRITICAL_SECTION) ---------- */ + +#ifdef _WIN32 +static SRWLOCK s_mutex = SRWLOCK_INIT; +static SRWLOCK s_render_mutex = SRWLOCK_INIT; +#define LOCK() AcquireSRWLockExclusive(&s_mutex) +#define UNLOCK() ReleaseSRWLockExclusive(&s_mutex) +#define RENDER_LOCK() AcquireSRWLockExclusive(&s_render_mutex) +#define RENDER_UNLOCK() ReleaseSRWLockExclusive(&s_render_mutex) +#else +static pthread_mutex_t s_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t s_render_mutex = PTHREAD_MUTEX_INITIALIZER; +#define LOCK() pthread_mutex_lock(&s_mutex) +#define UNLOCK() pthread_mutex_unlock(&s_mutex) +#define RENDER_LOCK() pthread_mutex_lock(&s_render_mutex) +#define RENDER_UNLOCK() pthread_mutex_unlock(&s_render_mutex) +#endif + +/* ---------- Shader cache path ---------- */ + +static void get_cache_path(char *buf, size_t len) +{ +#ifdef _WIN32 + char appdata[MAX_PATH] = {0}; + if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata))) { + snprintf(buf, len, "%s\\mlt\\placebo_shader_cache.bin", appdata); + } else { + buf[0] = '\0'; + } +#else + const char *home = getenv("HOME"); + if (home) { + snprintf(buf, len, "%s/.local/share/mlt/placebo_shader_cache.bin", home); + } else { + buf[0] = '\0'; + } +#endif +} + +static void load_cache(void) +{ + char path[512]; + get_cache_path(path, sizeof(path)); + if (path[0] == '\0') + return; + + FILE *f = fopen(path, "rb"); + if (!f) + return; + + fseek(f, 0, SEEK_END); + long size = ftell(f); + fseek(f, 0, SEEK_SET); + + if (size > 0) { + uint8_t *data = malloc(size); + if (data) { + if ((long) fread(data, 1, size, f) == size) { + pl_cache_load(s_cache, data, size); + mlt_log_info(NULL, + "[placebo] Loaded shader cache (%" PRId64 " bytes) from %s\n", + (int64_t) size, + path); + } + free(data); + } + } + fclose(f); +} + +static void ensure_cache_dir(const char *path) +{ + char dir[512]; + snprintf(dir, sizeof(dir), "%s", path); + +#ifdef _WIN32 + char *last_sep = strrchr(dir, '\\'); + if (last_sep) { + *last_sep = '\0'; + CreateDirectoryA(dir, NULL); + } +#else + char *last_sep = strrchr(dir, '/'); + if (last_sep) { + *last_sep = '\0'; + mkdir(dir, 0755); + } +#endif +} + +static void save_cache(void) +{ + if (!s_cache) + return; + + char path[512]; + get_cache_path(path, sizeof(path)); + if (path[0] == '\0') + return; + + ensure_cache_dir(path); + + size_t size = pl_cache_save(s_cache, NULL, 0); + if (size == 0) + return; + + uint8_t *data = malloc(size); + if (!data) + return; + + pl_cache_save(s_cache, data, size); + + FILE *f = fopen(path, "wb"); + if (f) { + fwrite(data, 1, size, f); + fclose(f); + mlt_log_info(NULL, + "[placebo] Saved shader cache (%" PRIu64 " bytes) to %s\n", + (uint64_t) size, + path); + } + free(data); +} + +/* ---------- GPU initialization ---------- */ + +static int init_gpu(void) +{ + /* Create logger */ + s_log = pl_log_create(PL_API_VER, pl_log_params(.log_level = PL_LOG_WARN, )); + if (!s_log) { + mlt_log_error(NULL, "[placebo] Failed to create pl_log\n"); + return 0; + } + + /* Create shader cache */ + s_cache = pl_cache_create(pl_cache_params(.log = s_log, )); + + /* Try D3D11 first on Windows */ +#ifdef PL_HAVE_D3D11 + s_d3d11 = pl_d3d11_create(s_log, pl_d3d11_params(.allow_software = false, )); + if (s_d3d11) { + s_gpu = s_d3d11->gpu; + mlt_log_info(NULL, "[placebo] GPU initialized via D3D11\n"); + goto done; + } + mlt_log_warning(NULL, "[placebo] D3D11 init failed, trying Vulkan...\n"); +#endif + + /* Try Vulkan */ +#ifdef PL_HAVE_VULKAN + { + PFN_vkGetInstanceProcAddr vk_get_proc = NULL; +#ifdef PL_HAVE_VK_PROC_ADDR + /* libplacebo was linked against the Vulkan loader */ + vk_get_proc = NULL; /* libplacebo will use its own */ +#elif defined(_WIN32) + /* Dynamically load vulkan-1.dll to get vkGetInstanceProcAddr */ + s_vulkan_dll = LoadLibraryA("vulkan-1.dll"); + if (s_vulkan_dll) { + vk_get_proc = (PFN_vkGetInstanceProcAddr) GetProcAddress(s_vulkan_dll, + "vkGetInstanceProcAddr"); + if (!vk_get_proc) { + mlt_log_warning( + NULL, "[placebo] vulkan-1.dll loaded but vkGetInstanceProcAddr not found\n"); + FreeLibrary(s_vulkan_dll); + s_vulkan_dll = NULL; + } + } else { + mlt_log_warning(NULL, "[placebo] vulkan-1.dll not found\n"); + } +#else + /* On Linux/macOS, dlopen the Vulkan loader */ + mlt_log_warning(NULL, + "[placebo] Vulkan: no VK_PROC_ADDR and no dynamic loader implemented\n"); +#endif + + if (vk_get_proc || 0 +#ifdef PL_HAVE_VK_PROC_ADDR + || 1 +#endif + ) { + s_vk_inst = pl_vk_inst_create(s_log, + pl_vk_inst_params(.get_proc_addr = vk_get_proc, + .debug = false, )); + if (s_vk_inst) { + s_vulkan = pl_vulkan_create(s_log, + pl_vulkan_params(.instance = s_vk_inst->instance, + .get_proc_addr = vk_get_proc, )); + if (s_vulkan) { + s_gpu = s_vulkan->gpu; + mlt_log_info(NULL, "[placebo] GPU initialized via Vulkan\n"); + goto done; + } + pl_vk_inst_destroy(&s_vk_inst); + s_vk_inst = NULL; + } + } + mlt_log_warning(NULL, "[placebo] Vulkan init failed\n"); + } +#endif + + /* Try OpenGL as last resort */ +#ifdef PL_HAVE_OPENGL + { + s_opengl = pl_opengl_create(s_log, pl_opengl_params()); + if (s_opengl) { + s_gpu = s_opengl->gpu; + mlt_log_info(NULL, "[placebo] GPU initialized via OpenGL\n"); + goto done; + } + mlt_log_warning(NULL, "[placebo] OpenGL init failed\n"); + } +#endif + + mlt_log_error(NULL, "[placebo] No GPU backend available\n"); + return 0; + +done: + if (s_cache) + pl_gpu_set_cache(s_gpu, s_cache); + + load_cache(); + + atexit(placebo_gpu_release); + + return 1; +} + +/* ---------- Public API ---------- */ + +pl_gpu placebo_gpu_get(void) +{ + LOCK(); + if (!s_initialized) { + s_initialized = 1; + if (!init_gpu()) { + /* Stay initialized=1 to prevent retry spam on every frame */ + UNLOCK(); + return NULL; + } + } + pl_gpu result = s_gpu; + UNLOCK(); + return result; +} + +pl_dispatch placebo_dispatch_get(void) +{ + pl_gpu gpu = placebo_gpu_get(); + if (!gpu) + return NULL; + + LOCK(); + if (!s_dispatch) { + s_dispatch = pl_dispatch_create(s_log, gpu); + } + pl_dispatch result = s_dispatch; + UNLOCK(); + return result; +} + +pl_renderer placebo_renderer_get(void) +{ + pl_gpu gpu = placebo_gpu_get(); + if (!gpu) + return NULL; + + LOCK(); + if (!s_renderer) { + s_renderer = pl_renderer_create(s_log, gpu); + } + pl_renderer result = s_renderer; + UNLOCK(); + return result; +} + +void placebo_render_lock(void) +{ + RENDER_LOCK(); +} + +void placebo_render_unlock(void) +{ + RENDER_UNLOCK(); +} + +/* ---------- Frame GPU texture reuse ---------- + * + * When multiple placebo filters are chained on one producer, each filter + * would normally upload the frame to GPU, render, and download back to RAM. + * For N filters that means N uploads and N downloads — the intermediate + * RAM roundtrips are pure waste because the next placebo filter will + * re-upload the same data immediately. + * + * To avoid this, each placebo filter attaches its output texture to the + * mlt_frame via put_tex(). The next placebo filter in the chain calls + * take_tex() to grab it and use it directly as its source, skipping the + * upload. The download to RAM still happens every time (MLT expects the + * image buffer to be current), but the upload is eliminated for all + * filters after the first one. + * + * Staleness detection: if a non-placebo CPU filter runs between two + * placebo filters, it may reallocate the image buffer (e.g. via + * mlt_frame_get_image with writable=1, which triggers a copy). + * put_tex() records the buffer pointer, and take_tex() compares it + * against the current pointer. A mismatch means the texture content + * no longer matches RAM, so take_tex() returns NULL and the caller + * falls back to a fresh upload. This is safe because MLT's standard + * mechanism for a filter to modify frame data is to request a writable + * buffer, which always produces a new allocation. + */ + +typedef struct +{ + pl_tex tex; + const uint8_t *image_ptr; /* RAM buffer address at time of put_tex */ +} placebo_frame_gpu; + +/* Called by mlt_properties when the frame is destroyed or the property + * is overwritten. Must acquire render_lock because pl_tex_destroy + * touches GPU state, and this destructor may fire from any thread. */ +static void frame_gpu_destroy(void *ptr) +{ + placebo_frame_gpu *d = ptr; + if (d && d->tex) { + pl_gpu gpu = placebo_gpu_get(); + if (gpu) { + placebo_render_lock(); + pl_tex_destroy(gpu, &d->tex); + placebo_render_unlock(); + } + } + free(d); +} + +pl_tex placebo_frame_take_tex(mlt_frame frame, const uint8_t *current_image) +{ + mlt_properties props = MLT_FRAME_PROPERTIES(frame); + placebo_frame_gpu *d = mlt_properties_get_data(props, "_placebo_gpu", NULL); + if (!d || !d->tex) + return NULL; + if (d->image_ptr != current_image) + return NULL; /* buffer was reallocated — texture is stale */ + pl_tex tex = d->tex; + d->tex = NULL; /* transfer ownership to caller; disarm destructor */ + return tex; +} + +void placebo_frame_put_tex(mlt_frame frame, pl_tex tex, const uint8_t *image_ptr) +{ + placebo_frame_gpu *d = calloc(1, sizeof(placebo_frame_gpu)); + d->tex = tex; + d->image_ptr = image_ptr; + /* If a previous texture was attached, set_data replaces it and + * frame_gpu_destroy fires for the old one. After take_tex() the + * old entry has tex=NULL so the destructor becomes a no-op. */ + mlt_properties_set_data(MLT_FRAME_PROPERTIES(frame), + "_placebo_gpu", + d, + 0, + frame_gpu_destroy, + NULL); +} + +/* ---------- GPU teardown ---------- */ + +void placebo_gpu_release(void) +{ + LOCK(); + if (!s_initialized) { + UNLOCK(); + return; + } + + save_cache(); + + if (s_renderer) { + pl_renderer_destroy(&s_renderer); + s_renderer = NULL; + } + if (s_dispatch) { + pl_dispatch_destroy(&s_dispatch); + s_dispatch = NULL; + } + + if (s_cache) { + pl_cache_destroy(&s_cache); + s_cache = NULL; + } + +#ifdef PL_HAVE_D3D11 + if (s_d3d11) { + pl_d3d11_destroy(&s_d3d11); + s_d3d11 = NULL; + } +#endif + +#ifdef PL_HAVE_VULKAN + if (s_vulkan) { + pl_vulkan_destroy(&s_vulkan); + s_vulkan = NULL; + } + if (s_vk_inst) { + pl_vk_inst_destroy(&s_vk_inst); + s_vk_inst = NULL; + } +#ifdef _WIN32 + if (s_vulkan_dll) { + FreeLibrary(s_vulkan_dll); + s_vulkan_dll = NULL; + } +#endif +#endif + +#ifdef PL_HAVE_OPENGL + if (s_opengl) { + pl_opengl_destroy(&s_opengl); + s_opengl = NULL; + } +#endif + + if (s_log) { + pl_log_destroy(&s_log); + s_log = NULL; + } + + s_gpu = NULL; + s_initialized = 0; + + UNLOCK(); +} diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h new file mode 100644 index 000000000..6c680fcb2 --- /dev/null +++ b/src/modules/placebo/gpu_context.h @@ -0,0 +1,61 @@ +/* + * gpu_context.h -- shared libplacebo GPU lifecycle + * Copyright (C) 2025 D-Ogi + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef GPU_CONTEXT_H +#define GPU_CONTEXT_H + +#include +#include +#include +#include + +#include + +/* Return the singleton pl_gpu, lazily initialized on first call. + Returns NULL on failure. Thread-safe. */ +pl_gpu placebo_gpu_get(void); + +/* Return the singleton pl_dispatch (for shader filter). Thread-safe. */ +pl_dispatch placebo_dispatch_get(void); + +/* Return the singleton pl_renderer (for render filter). Thread-safe. */ +pl_renderer placebo_renderer_get(void); + +/* Lock/unlock the render mutex. pl_renderer and pl_dispatch are NOT + thread-safe, so callers must hold this lock during pl_render_image + and pl_dispatch calls. */ +void placebo_render_lock(void); +void placebo_render_unlock(void); + +/* Tear down all GPU resources. Registered via atexit on first init. */ +void placebo_gpu_release(void); + +/* Retrieve a GPU texture left on the frame by a preceding placebo filter. + Returns NULL if none exists, or if the image buffer pointer differs from + current_image (indicating an intervening CPU filter reallocated it). + On success, ownership transfers to the caller who must destroy it. */ +pl_tex placebo_frame_take_tex(mlt_frame frame, const uint8_t *current_image); + +/* Attach a rendered GPU texture to the frame so the next placebo filter + in the chain can skip the RAM-to-GPU upload. image_ptr records the + current buffer address for staleness detection (see take_tex above). + The texture is destroyed automatically if no filter claims it. */ +void placebo_frame_put_tex(mlt_frame frame, pl_tex tex, const uint8_t *image_ptr); + +#endif /* GPU_CONTEXT_H */