From ebe1caefca5bfa0a46a835e1666ab3581662c2a0 Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Sun, 1 Feb 2026 22:33:15 +0100 Subject: [PATCH 01/24] Add libplacebo GPU module with render and shader filters New module 'placebo' providing GPU-accelerated video processing via libplacebo. Includes two filters: - placebo.render: GPU scaling, debanding, dithering, and tonemapping with quality presets (fast/default/high_quality) - placebo.shader: Custom mpv-compatible .hook shader support Backend priority: D3D11 (Windows) -> Vulkan -> OpenGL. Vulkan loader is dynamically loaded on Windows when libplacebo is built without vk-proc-addr support. Features: - Singleton GPU context with thread-safe access - Shader cache persistence - Multiple scaling algorithms (ewa_lanczos, lanczos, mitchell, etc.) - Tone mapping (auto, clip, mobius, reinhard, hable, bt.2390, spline) - Graceful fallback to passthrough when no GPU is available The module is enabled by default but skipped automatically when libplacebo is not installed. --- .github/workflows/static-code-analysis.yml | 3 + CMakeLists.txt | 11 + src/modules/CMakeLists.txt | 4 + src/modules/placebo/CMakeLists.txt | 55 +++ src/modules/placebo/factory.c | 53 +++ src/modules/placebo/filter_placebo_render.c | 273 +++++++++++ src/modules/placebo/filter_placebo_render.yml | 89 ++++ src/modules/placebo/filter_placebo_shader.c | 355 ++++++++++++++ src/modules/placebo/filter_placebo_shader.yml | 38 ++ src/modules/placebo/gpu_context.c | 433 ++++++++++++++++++ src/modules/placebo/gpu_context.h | 47 ++ 11 files changed, 1361 insertions(+) create mode 100644 src/modules/placebo/CMakeLists.txt create mode 100644 src/modules/placebo/factory.c create mode 100644 src/modules/placebo/filter_placebo_render.c create mode 100644 src/modules/placebo/filter_placebo_render.yml create mode 100644 src/modules/placebo/filter_placebo_shader.c create mode 100644 src/modules/placebo/filter_placebo_shader.yml create mode 100644 src/modules/placebo/gpu_context.c create mode 100644 src/modules/placebo/gpu_context.h 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..95eb50cae --- /dev/null +++ b/src/modules/placebo/CMakeLists.txt @@ -0,0 +1,55 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo) +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..3e42fba7f --- /dev/null +++ b/src/modules/placebo/filter_placebo_render.c @@ -0,0 +1,273 @@ +/* + * 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); + } + + /* W2 fix: verify RGBA8 format is supported */ + 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; + + /* W4 fix: hold render lock for all GPU operations */ + placebo_render_lock(); + + /* Create source texture and upload */ + pl_tex 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; /* pass through original frame data */ + } + + /* C4 fix: check upload return */ + 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; + } + + /* Create destination texture */ + pl_tex dst_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .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"); + } + + /* C4 fix: check download return */ + 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"); + } + + /* Cleanup textures */ + pl_tex_destroy(gpu, &src_tex); + pl_tex_destroy(gpu, &dst_tex); + + 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..8d55ed868 --- /dev/null +++ b/src/modules/placebo/filter_placebo_shader.c @@ -0,0 +1,355 @@ +/* + * 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 + +#ifdef _WIN32 +#include +/* Use _stat on Windows to avoid deprecation warnings (S5) */ +#include +#define stat_func _stat +#define stat_struct _stat +#else +#include +#define stat_func stat +#define stat_struct stat +#endif + +/* 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; + +/* C3 fix: single cleanup function used both by filter_close and mlt_properties destructor */ +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) { /* W7 fix: reject files > 10 MB */ + 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; +} + +/* 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; + + priv->hooks[0] = pl_mpv_user_shader_parse(gpu, shader_text, strlen(shader_text)); + if (!priv->hooks[0]) { + mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to parse inline shader_text\n"); + return 0; + } + + priv->num_hooks = 1; + priv->loaded_text = strdup(shader_text); + mlt_log_info(MLT_FILTER_SERVICE(filter), + "Loaded inline shader (%zu bytes)\n", + strlen(shader_text)); + } + 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); + } + + /* W2 fix: verify RGBA8 format is supported */ + 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); + } + + /* Get or create private data (C3 fix: single destructor path) */ + 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; + + /* W4 fix: hold render lock for all GPU operations */ + placebo_render_lock(); + + /* Create source texture and upload */ + pl_tex 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; + } + + /* C4 fix: check upload return */ + 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; + } + + /* Create destination texture */ + pl_tex dst_tex = pl_tex_create(gpu, + pl_tex_params(.w = w, + .h = h, + .format = rgba_fmt, + .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, + }; + + /* 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"); + } + + /* C4 fix: check download return */ + 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"); + } + + /* Cleanup */ + pl_tex_destroy(gpu, &src_tex); + pl_tex_destroy(gpu, &dst_tex); + + 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; + /* C3 fix: no filter->close needed -- cleanup via mlt_properties destructor */ + 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..da260327f --- /dev/null +++ b/src/modules/placebo/gpu_context.c @@ -0,0 +1,433 @@ +/* + * 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 + +#ifdef PL_HAVE_D3D11 +#include +#endif + +#ifdef PL_HAVE_VULKAN +#include +#endif + +#ifdef PL_HAVE_OPENGL +#include +#endif + +#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; /* stored to avoid leak (W1) */ +#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 (C1 fix: use SRWLOCK on Windows -- statically initializable) ---------- */ + +#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 (%ld bytes) from %s\n", + 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); /* W5 fix: actually create the directory */ + } +#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 (%zu bytes) to %s\n", 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(); + + /* C2 fix: register cleanup at process exit */ + 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; +} + +/* W4 fix: render lock for thread safety around pl_renderer/pl_dispatch calls */ +void placebo_render_lock(void) +{ + RENDER_LOCK(); +} + +void placebo_render_unlock(void) +{ + RENDER_UNLOCK(); +} + +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; + } + /* W1 fix: destroy Vulkan instance */ + 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..36f12e9d7 --- /dev/null +++ b/src/modules/placebo/gpu_context.h @@ -0,0 +1,47 @@ +/* + * 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 + +/* 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); + +#endif /* GPU_CONTEXT_H */ From 35e85eb1eb0b0867fb016ec4567797c4e2c4fb7c Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Mon, 2 Feb 2026 16:07:06 +0100 Subject: [PATCH 02/24] Fix MinGW build: portable format specifiers, add libplacebo to CI Use PRIu64/PRId64 from instead of %zu/%ld for size logging in the placebo module. Add libplacebo-dev packages to Ubuntu, Debian, and Fedora 42 CI workflows, and mingw-w64-x86_64-libplacebo to the MSYS2 MinGW64 workflow. --- .github/workflows/build-distros.yml | 12 +++++++----- .github/workflows/build-linux.yml | 2 +- .github/workflows/build-msys2-mingw64.yml | 1 + src/modules/placebo/CMakeLists.txt | 2 +- src/modules/placebo/filter_placebo_shader.c | 5 +++-- src/modules/placebo/gpu_context.c | 7 ++++--- 6 files changed, 17 insertions(+), 12 deletions(-) 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/src/modules/placebo/CMakeLists.txt b/src/modules/placebo/CMakeLists.txt index 95eb50cae..92775505c 100644 --- a/src/modules/placebo/CMakeLists.txt +++ b/src/modules/placebo/CMakeLists.txt @@ -1,5 +1,5 @@ find_package(PkgConfig REQUIRED) -pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo) +pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo>=5.229) if(NOT libplacebo_FOUND) return() endif() diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index 8d55ed868..7e5d8102e 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -27,6 +27,7 @@ #include #include +#include #include #include #include @@ -183,8 +184,8 @@ static int ensure_shader(mlt_filter filter, shader_private *priv, pl_gpu gpu) priv->num_hooks = 1; priv->loaded_text = strdup(shader_text); mlt_log_info(MLT_FILTER_SERVICE(filter), - "Loaded inline shader (%zu bytes)\n", - strlen(shader_text)); + "Loaded inline shader (%" PRIu64 " bytes)\n", + (uint64_t) strlen(shader_text)); } return priv->num_hooks > 0; } diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index da260327f..313d96787 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -39,6 +39,7 @@ #include +#include #include #include #include @@ -139,8 +140,8 @@ static void load_cache(void) if ((long) fread(data, 1, size, f) == size) { pl_cache_load(s_cache, data, size); mlt_log_info(NULL, - "[placebo] Loaded shader cache (%ld bytes) from %s\n", - size, + "[placebo] Loaded shader cache (%" PRId64 " bytes) from %s\n", + (int64_t) size, path); } free(data); @@ -195,7 +196,7 @@ static void save_cache(void) if (f) { fwrite(data, 1, size, f); fclose(f); - mlt_log_info(NULL, "[placebo] Saved shader cache (%zu bytes) to %s\n", size, path); + mlt_log_info(NULL, "[placebo] Saved shader cache (%" PRIu64 " bytes) to %s\n", (uint64_t) size, path); } free(data); } From 394d73cc85b26f07f783c58f1c7a8e15df2a35af Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Tue, 3 Feb 2026 10:36:11 +0100 Subject: [PATCH 03/24] Fix clang-format violation in save_cache log line Break long mlt_log_info() call into multi-line format to match the project's clang-format rules (same style as load_cache above). --- src/modules/placebo/gpu_context.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 313d96787..31b3e1d2b 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -196,7 +196,10 @@ static void save_cache(void) 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); + mlt_log_info(NULL, + "[placebo] Saved shader cache (%" PRIu64 " bytes) to %s\n", + (uint64_t) size, + path); } free(data); } From 32f8a4730e77fe28cf841b9b395c973558b52502 Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Thu, 5 Feb 2026 00:01:11 +0100 Subject: [PATCH 04/24] Reuse GPU textures between chained placebo filters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple placebo filters are stacked on one clip, each filter previously did a full RAM→GPU upload and GPU→RAM download. The intermediate uploads are redundant because the next placebo filter would re-upload the same pixels immediately. Each filter now attaches its output texture to the mlt_frame via placebo_frame_put_tex(). The next placebo filter calls placebo_frame_take_tex() to grab it directly as source, skipping the upload. The download to RAM still happens every time (MLT expects the image buffer to be current for non-GPU filters). Staleness detection: put_tex records the RAM buffer pointer, take_tex compares it against the current pointer. If a CPU filter ran in between and requested a writable buffer (triggering a copy and new allocation), the pointers differ and take_tex returns NULL, falling back to a fresh upload. Also cleans up internal ticket-style comments (C1/W2/etc.) with descriptions of actual logic and pitfalls. --- src/modules/placebo/filter_placebo_render.c | 67 +++++++++------- src/modules/placebo/filter_placebo_shader.c | 77 +++++++++++-------- src/modules/placebo/gpu_context.c | 85 +++++++++++++++++++-- src/modules/placebo/gpu_context.h | 14 ++++ 4 files changed, 178 insertions(+), 65 deletions(-) diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index 3e42fba7f..49f754a08 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -99,7 +99,7 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* W2 fix: verify RGBA8 format is supported */ + /* 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"); @@ -116,38 +116,48 @@ static int filter_get_image(mlt_frame frame, int h = *height; size_t stride = w * 4; - /* W4 fix: hold render lock for all GPU operations */ + /* pl_renderer is not thread-safe; hold the lock for the entire + * upload → render → download sequence */ placebo_render_lock(); - /* Create source texture and upload */ - pl_tex 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; /* pass through original frame data */ - } + /* 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; + } - /* C4 fix: check upload return */ - 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; + 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; + } } - /* Create destination texture */ + /* 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) { @@ -228,7 +238,8 @@ static int filter_get_image(mlt_frame frame, mlt_log_warning(MLT_FILTER_SERVICE(filter), "pl_render_image failed\n"); } - /* C4 fix: check download return */ + /* 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, @@ -236,9 +247,11 @@ static int filter_get_image(mlt_frame frame, mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU texture download failed\n"); } - /* Cleanup textures */ pl_tex_destroy(gpu, &src_tex); - pl_tex_destroy(gpu, &dst_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(); diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index 7e5d8102e..ef8524770 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -34,7 +34,7 @@ #ifdef _WIN32 #include -/* Use _stat on Windows to avoid deprecation warnings (S5) */ +/* MSVC marks stat() as deprecated; _stat is the replacement */ #include #define stat_func _stat #define stat_struct _stat @@ -54,7 +54,8 @@ typedef struct char *loaded_text; /* inline text that was loaded */ } shader_private; -/* C3 fix: single cleanup function used both by filter_close and mlt_properties destructor */ +/* 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; @@ -81,7 +82,7 @@ static char *read_file(const char *path, size_t *out_len) long len = ftell(f); fseek(f, 0, SEEK_SET); - if (len <= 0 || len > 10 * 1024 * 1024) { /* W7 fix: reject files > 10 MB */ + if (len <= 0 || len > 10 * 1024 * 1024) { /* sanity limit for shader files */ fclose(f); return NULL; } @@ -210,14 +211,14 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* W2 fix: verify RGBA8 format is supported */ + /* 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); } - /* Get or create private data (C3 fix: single destructor path) */ + /* 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)); @@ -242,38 +243,48 @@ static int filter_get_image(mlt_frame frame, int h = *height; size_t stride = w * 4; - /* W4 fix: hold render lock for all GPU operations */ + /* pl_renderer is not thread-safe; hold the lock for the entire + * upload → render → download sequence */ placebo_render_lock(); - /* Create source texture and upload */ - pl_tex 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; - } + /* 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; + } - /* C4 fix: check upload return */ - 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; + 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; + } } - /* Create destination texture */ + /* 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) { @@ -315,7 +326,8 @@ static int filter_get_image(mlt_frame frame, mlt_log_warning(MLT_FILTER_SERVICE(filter), "pl_render_image with shader hook failed\n"); } - /* C4 fix: check download return */ + /* 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, @@ -323,9 +335,11 @@ static int filter_get_image(mlt_frame frame, mlt_log_warning(MLT_FILTER_SERVICE(filter), "GPU texture download failed\n"); } - /* Cleanup */ pl_tex_destroy(gpu, &src_tex); - pl_tex_destroy(gpu, &dst_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(); @@ -347,7 +361,6 @@ mlt_filter filter_placebo_shader_init(mlt_profile profile, mlt_filter filter = mlt_filter_new(); if (filter) { filter->process = filter_process; - /* C3 fix: no filter->close needed -- cleanup via mlt_properties destructor */ mlt_properties props = MLT_FILTER_PROPERTIES(filter); mlt_properties_set(props, "shader_path", arg ? arg : ""); mlt_properties_set(props, "shader_text", ""); diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 31b3e1d2b..bb85ccf91 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -19,6 +19,7 @@ #include "gpu_context.h" +#include #include #include #include @@ -63,7 +64,7 @@ static pl_d3d11 s_d3d11 = NULL; #ifdef PL_HAVE_VULKAN static pl_vulkan s_vulkan = NULL; -static pl_vk_inst s_vk_inst = NULL; /* stored to avoid leak (W1) */ +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 @@ -80,7 +81,7 @@ static pl_cache s_cache = NULL; static int s_initialized = 0; -/* ---------- Mutex (C1 fix: use SRWLOCK on Windows -- statically initializable) ---------- */ +/* ---------- Mutex (SRWLOCK on Windows is statically initializable, unlike CRITICAL_SECTION) ---------- */ #ifdef _WIN32 static SRWLOCK s_mutex = SRWLOCK_INIT; @@ -165,7 +166,7 @@ static void ensure_cache_dir(const char *path) char *last_sep = strrchr(dir, '/'); if (last_sep) { *last_sep = '\0'; - mkdir(dir, 0755); /* W5 fix: actually create the directory */ + mkdir(dir, 0755); } #endif } @@ -304,7 +305,6 @@ static int init_gpu(void) load_cache(); - /* C2 fix: register cleanup at process exit */ atexit(placebo_gpu_release); return 1; @@ -358,7 +358,6 @@ pl_renderer placebo_renderer_get(void) return result; } -/* W4 fix: render lock for thread safety around pl_renderer/pl_dispatch calls */ void placebo_render_lock(void) { RENDER_LOCK(); @@ -369,6 +368,81 @@ 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(); @@ -405,7 +479,6 @@ void placebo_gpu_release(void) pl_vulkan_destroy(&s_vulkan); s_vulkan = NULL; } - /* W1 fix: destroy Vulkan instance */ if (s_vk_inst) { pl_vk_inst_destroy(&s_vk_inst); s_vk_inst = NULL; diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 36f12e9d7..6c680fcb2 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -25,6 +25,8 @@ #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); @@ -44,4 +46,16 @@ 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 */ From d687bdfc15671941e1e397492350dc8f919a53e1 Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Thu, 5 Feb 2026 00:33:36 +0100 Subject: [PATCH 05/24] Support DYNAMIC shader parameters and base64-encoded shader_text Add apply_shader_params() to override pl_hook DYNAMIC parameters from MLT animated properties (shader_param.* prefix) on every frame. Uses mlt_properties_anim_get_double/int to correctly resolve keyframe strings ("0=200;50=100") at the current frame position. Add base64 decoding for shader_text values prefixed with "base64:" to support inline shaders with characters that are problematic in MLT property strings. --- src/modules/placebo/filter_placebo_shader.c | 130 +++++++++++++++++++- 1 file changed, 127 insertions(+), 3 deletions(-) diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index ef8524770..f4f1a7a77 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -44,6 +44,45 @@ #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 { @@ -113,6 +152,64 @@ static time_t file_mtime(const char *path) 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) { @@ -176,17 +273,37 @@ static int ensure_shader(mlt_filter filter, shader_private *priv, pl_gpu gpu) free(priv->loaded_text); priv->loaded_text = NULL; - priv->hooks[0] = pl_mpv_user_shader_parse(gpu, shader_text, strlen(shader_text)); + /* 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)\n", - (uint64_t) strlen(shader_text)); + "Loaded inline shader (%" PRIu64 " bytes%s)\n", + (uint64_t) parse_len, + decoded ? ", base64-decoded" : ""); + free(decoded); } return priv->num_hooks > 0; } @@ -317,6 +434,13 @@ static int filter_get_image(mlt_frame frame, .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; From cc0296153a746767af5f5d77a4777d1c993dd90b Mon Sep 17 00:00:00 2001 From: D-Ogi Date: Thu, 5 Feb 2026 01:24:41 +0100 Subject: [PATCH 06/24] Fix clang-format-14 violations in placebo module Run clang-format-14 (matching CI) on filter_placebo_shader.c and gpu_context.c to fix designated initializer spacing, ternary line breaks, and long argument lists. --- src/modules/placebo/filter_placebo_shader.c | 38 ++++++++++++--------- src/modules/placebo/gpu_context.c | 7 +++- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index f4f1a7a77..bcc7e3063 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -46,14 +46,14 @@ /* ---- 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, + ['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. */ @@ -67,11 +67,16 @@ static long b64_decode(const char *src, size_t src_len, char *dst) i++; continue; } - if (i + 1 >= src_len) break; + 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; + 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)); @@ -195,14 +200,13 @@ static void apply_shader_params(mlt_filter filter, * 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); + 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); + 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); + par->data->u + = (unsigned) mlt_properties_anim_get_int(props, key, position, length); } break; } diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index bb85ccf91..3c6b85b9e 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -438,7 +438,12 @@ void placebo_frame_put_tex(mlt_frame frame, pl_tex tex, const uint8_t *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); + mlt_properties_set_data(MLT_FRAME_PROPERTIES(frame), + "_placebo_gpu", + d, + 0, + frame_gpu_destroy, + NULL); } /* ---------- GPU teardown ---------- */ From 3b112bba4242b37db7b6c909014665f3e83f1a9b Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 17:54:04 -0700 Subject: [PATCH 07/24] add MLT_PLACEBO_CACHE_PATH and replace atext() --- src/modules/placebo/factory.c | 2 +- src/modules/placebo/filter_placebo_render.c | 2 +- src/modules/placebo/filter_placebo_render.yml | 5 ++++- src/modules/placebo/filter_placebo_shader.c | 2 +- src/modules/placebo/filter_placebo_shader.yml | 5 ++++- src/modules/placebo/gpu_context.c | 10 ++++++++-- src/modules/placebo/gpu_context.h | 2 +- 7 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/modules/placebo/factory.c b/src/modules/placebo/factory.c index 413d73480..be8c8bde0 100644 --- a/src/modules/placebo/factory.c +++ b/src/modules/placebo/factory.c @@ -1,6 +1,6 @@ /* * factory.c -- module registration for libplacebo filters - * Copyright (C) 2025 D-Ogi + * Copyright (C) 2025-2026 D-Ogi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index 49f754a08..1e859fd00 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -1,6 +1,6 @@ /* * filter_placebo_render.c -- GPU-accelerated renderer via libplacebo - * Copyright (C) 2025 D-Ogi + * Copyright (C) 2025-2026 D-Ogi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/src/modules/placebo/filter_placebo_render.yml b/src/modules/placebo/filter_placebo_render.yml index 2420a4e3c..6733ca06d 100644 --- a/src/modules/placebo/filter_placebo_render.yml +++ b/src/modules/placebo/filter_placebo_render.yml @@ -3,7 +3,7 @@ type: filter identifier: placebo.render title: GPU Render (libplacebo) version: 1 -copyright: Copyright (C) 2025 D-Ogi +copyright: Copyright (C) 2025-2026 D-Ogi creator: D-Ogi license: LGPLv2.1 language: en @@ -13,6 +13,9 @@ 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. + The compiled shader cache is stored in a platform-specific location and can + be overridden by setting the MLT_PLACEBO_CACHE_PATH environment variable to + an absolute path for the cache file. parameters: - identifier: preset diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index bcc7e3063..a32131180 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -1,6 +1,6 @@ /* * filter_placebo_shader.c -- custom .hook shader loader via libplacebo - * Copyright (C) 2025 D-Ogi + * Copyright (C) 2025-2026 D-Ogi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public diff --git a/src/modules/placebo/filter_placebo_shader.yml b/src/modules/placebo/filter_placebo_shader.yml index 22da5ae95..fe2a61428 100644 --- a/src/modules/placebo/filter_placebo_shader.yml +++ b/src/modules/placebo/filter_placebo_shader.yml @@ -3,7 +3,7 @@ type: filter identifier: placebo.shader title: GPU Shader (libplacebo) version: 1 -copyright: Copyright (C) 2025 D-Ogi +copyright: Copyright (C) 2025-2026 D-Ogi creator: D-Ogi license: LGPLv2.1 language: en @@ -13,6 +13,9 @@ 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. + The compiled shader cache is stored in a platform-specific location and can + be overridden by setting the MLT_PLACEBO_CACHE_PATH environment variable to + an absolute path for the cache file. parameters: - identifier: shader_path diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 3c6b85b9e..c32cfad97 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -1,6 +1,6 @@ /* * gpu_context.c -- shared libplacebo GPU lifecycle (singleton) - * Copyright (C) 2025 D-Ogi + * Copyright (C) 2025-2026 D-Ogi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -19,6 +19,7 @@ #include "gpu_context.h" +#include #include #include #include @@ -103,6 +104,11 @@ static pthread_mutex_t s_render_mutex = PTHREAD_MUTEX_INITIALIZER; static void get_cache_path(char *buf, size_t len) { + const char *override = getenv("MLT_PLACEBO_CACHE_PATH"); + if (override && override[0] != '\0') { + snprintf(buf, len, "%s", override); + return; + } #ifdef _WIN32 char appdata[MAX_PATH] = {0}; if (SUCCEEDED(SHGetFolderPathA(NULL, CSIDL_APPDATA, NULL, 0, appdata))) { @@ -305,7 +311,7 @@ static int init_gpu(void) load_cache(); - atexit(placebo_gpu_release); + mlt_factory_register_for_clean_up(NULL, (mlt_destructor) placebo_gpu_release); return 1; } diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 6c680fcb2..3bf2818fb 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -1,6 +1,6 @@ /* * gpu_context.h -- shared libplacebo GPU lifecycle - * Copyright (C) 2025 D-Ogi + * Copyright (C) 2025-2026 D-Ogi * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public From ce993648a2489a709affdb1edd3df898780a8151 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Tue, 5 May 2026 22:29:46 -0700 Subject: [PATCH 08/24] Add placebo.convert filter - Introduced new `mlt_image_private` format for module internal use. - Improved placebo texture reuse across filters. --- src/framework/mlt_frame.c | 17 +- src/framework/mlt_image.c | 12 +- src/framework/mlt_types.h | 5 +- src/modules/avformat/common.c | 2 + src/modules/avformat/filter_avcolour_space.c | 8 + .../avformat/filter_sws_colortransform.c | 2 +- src/modules/avformat/link_avdeinterlace.c | 1 + src/modules/avformat/producer_avformat.c | 6 +- src/modules/core/filter_brightness.c | 1 + src/modules/core/filter_color_transform.c | 4 +- src/modules/core/loader.ini | 2 +- src/modules/core/producer_colour.c | 5 +- src/modules/placebo/CMakeLists.txt | 2 + src/modules/placebo/factory.c | 9 + src/modules/placebo/filter_placebo_convert.c | 174 ++++++++++++++++++ .../placebo/filter_placebo_convert.yml | 14 ++ src/modules/placebo/filter_placebo_render.c | 76 +++----- src/modules/placebo/filter_placebo_shader.c | 78 +++----- src/modules/placebo/gpu_context.c | 109 +++++------ src/modules/placebo/gpu_context.h | 27 +-- 20 files changed, 352 insertions(+), 202 deletions(-) create mode 100644 src/modules/placebo/filter_placebo_convert.c create mode 100644 src/modules/placebo/filter_placebo_convert.yml diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index 5a5536cf4..f07749f6f 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -448,6 +448,7 @@ static int generate_test_image(mlt_properties properties, case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: *format = mlt_image_yuv422; break; case mlt_image_invalid: @@ -1113,15 +1114,13 @@ mlt_frame mlt_frame_clone(mlt_frame self, int is_deep) } size = 0; data = mlt_properties_get_data(properties, "image", &size); - if (data && mlt_image_movit != mlt_properties_get_int(properties, "format")) { + mlt_image_format format = mlt_properties_get_int(properties, "format"); + if (data && format != mlt_image_movit && format != mlt_image_private) { int width = mlt_properties_get_int(properties, "width"); int height = mlt_properties_get_int(properties, "height"); if (!size) - size = mlt_image_format_size(mlt_properties_get_int(properties, "format"), - width, - height, - NULL); + size = mlt_image_format_size(format, width, height, NULL); copy = mlt_pool_alloc(size); memcpy(copy, data, size); mlt_properties_set_data(new_props, "image", copy, size, mlt_pool_release, NULL); @@ -1255,15 +1254,13 @@ mlt_frame mlt_frame_clone_image(mlt_frame self, int is_deep) if (is_deep) { data = mlt_properties_get_data(properties, "image", &size); - if (data && mlt_image_movit != mlt_properties_get_int(properties, "format")) { + mlt_image_format format = mlt_properties_get_int(properties, "format"); + if (data && format != mlt_image_movit && format != mlt_image_private) { int width = mlt_properties_get_int(properties, "width"); int height = mlt_properties_get_int(properties, "height"); if (!size) - size = mlt_image_format_size(mlt_properties_get_int(properties, "format"), - width, - height, - NULL); + size = mlt_image_format_size(format, width, height, NULL); copy = mlt_pool_alloc(size); memcpy(copy, data, size); mlt_properties_set_data(new_props, "image", copy, size, mlt_pool_release, NULL); diff --git a/src/framework/mlt_image.c b/src/framework/mlt_image.c index 2a91e680f..034c7ce67 100644 --- a/src/framework/mlt_image.c +++ b/src/framework/mlt_image.c @@ -188,7 +188,8 @@ int mlt_image_calculate_size(mlt_image self) return self->width * self->height * 3 / 2; case mlt_image_movit: case mlt_image_opengl_texture: - return 4; + case mlt_image_private: + return sizeof(void *); case mlt_image_yuv422p16: return self->width * self->height * 4; case mlt_image_yuv420p10: @@ -228,6 +229,8 @@ const char *mlt_image_format_name(mlt_image_format format) return "glsl"; case mlt_image_opengl_texture: return "opengl_texture"; + case mlt_image_private: + return "private"; case mlt_image_yuv422p16: return "yuv422p16"; case mlt_image_yuv420p10: @@ -489,6 +492,7 @@ mlt_colorspace mlt_image_default_colorspace(mlt_image_format format, int height) case mlt_image_rgba64: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: colorspace = mlt_colorspace_rgb; break; case mlt_image_yuv422: @@ -605,6 +609,7 @@ void mlt_image_fill_black(mlt_image self) case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: return; case mlt_image_rgb: case mlt_image_rgba: @@ -693,6 +698,7 @@ void mlt_image_fill_checkerboard(mlt_image self, double sample_aspect_ratio) case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: return; case mlt_image_rgb: case mlt_image_rgba: { @@ -798,6 +804,7 @@ void mlt_image_fill_white(mlt_image self, int full_range) case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: return; case mlt_image_rgb: case mlt_image_rgba: @@ -952,9 +959,10 @@ int mlt_image_format_size(mlt_image_format format, int width, int height, int *b return width * height * 3 / 2; case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: if (bpp) *bpp = 0; - return 4; + return sizeof(void *); case mlt_image_yuv422p16: if (bpp) *bpp = 4; diff --git a/src/framework/mlt_types.h b/src/framework/mlt_types.h index 23f0c1884..2854183d1 100644 --- a/src/framework/mlt_types.h +++ b/src/framework/mlt_types.h @@ -2,7 +2,7 @@ * \file mlt_types.h * \brief Provides forward definitions of all public types * - * Copyright (C) 2003-2025 Meltytech, LLC + * Copyright (C) 2003-2026 Meltytech, LLC * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -49,7 +49,8 @@ typedef enum { mlt_image_yuv420p10, /**< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian */ mlt_image_yuv444p10, /**< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian */ mlt_image_rgba64, /**< 16-bit RGB with alpha channel */ - mlt_image_invalid + mlt_image_invalid, + mlt_image_private /**< for module internal use only */ } mlt_image_format; /** The set of supported audio formats */ diff --git a/src/modules/avformat/common.c b/src/modules/avformat/common.c index e3b1c4203..96b5988f5 100644 --- a/src/modules/avformat/common.c +++ b/src/modules/avformat/common.c @@ -322,6 +322,7 @@ int mlt_to_av_image_format(mlt_image_format format) return AV_PIX_FMT_RGBA64LE; case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: case mlt_image_invalid: mlt_log_error(NULL, "[filter_avfilter] Unexpected image format: %s\n", @@ -339,6 +340,7 @@ mlt_image_format mlt_get_supported_image_format(mlt_image_format format) case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: case mlt_image_rgba: return mlt_image_rgba; case mlt_image_rgb: diff --git a/src/modules/avformat/filter_avcolour_space.c b/src/modules/avformat/filter_avcolour_space.c index f04d7cf7c..f61f74aa8 100644 --- a/src/modules/avformat/filter_avcolour_space.c +++ b/src/modules/avformat/filter_avcolour_space.c @@ -153,6 +153,14 @@ static int convert_image(mlt_frame frame, mlt_properties_clear(properties, "convert_image_height"); if (*format != output_format || out_width) { + if (*format == mlt_image_none || *format == mlt_image_movit + || *format == mlt_image_opengl_texture || *format == mlt_image_private + || *format == mlt_image_invalid || output_format == mlt_image_none + || output_format == mlt_image_movit || output_format == mlt_image_opengl_texture + || output_format == mlt_image_private || output_format == mlt_image_invalid) { + return 1; + } + mlt_profile profile = mlt_service_profile( MLT_PRODUCER_SERVICE(mlt_frame_get_original_producer(frame))); int width = mlt_properties_get_int(properties, "width"); diff --git a/src/modules/avformat/filter_sws_colortransform.c b/src/modules/avformat/filter_sws_colortransform.c index 9e7fbe125..fa8c7d0da 100644 --- a/src/modules/avformat/filter_sws_colortransform.c +++ b/src/modules/avformat/filter_sws_colortransform.c @@ -54,7 +54,7 @@ static int filter_get_image(mlt_frame frame, // Only process if we have a valid image if (!*image || *format == mlt_image_none || *format == mlt_image_movit - || *format == mlt_image_opengl_texture) + || *format == mlt_image_opengl_texture || *format == mlt_image_private) return 0; // Get the current color transfer characteristics diff --git a/src/modules/avformat/link_avdeinterlace.c b/src/modules/avformat/link_avdeinterlace.c index 1094d6520..c945603a9 100644 --- a/src/modules/avformat/link_avdeinterlace.c +++ b/src/modules/avformat/link_avdeinterlace.c @@ -82,6 +82,7 @@ static mlt_image_format validate_format(mlt_image_format format) case mlt_image_none: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: case mlt_image_invalid: ret_format = mlt_image_yuv422; break; diff --git a/src/modules/avformat/producer_avformat.c b/src/modules/avformat/producer_avformat.c index c4bda0d53..27e899bb1 100644 --- a/src/modules/avformat/producer_avformat.c +++ b/src/modules/avformat/producer_avformat.c @@ -827,8 +827,9 @@ static mlt_image_format pick_image_format(enum AVPixelFormat pix_fmt, mlt_image_format current_format) { if (current_format == mlt_image_none || current_format == mlt_image_movit - || pix_fmt == AV_PIX_FMT_ARGB || pix_fmt == AV_PIX_FMT_RGBA || pix_fmt == AV_PIX_FMT_ABGR - || pix_fmt == AV_PIX_FMT_BGRA || pix_fmt == AV_PIX_FMT_GBRAP) { + || current_format == mlt_image_private || pix_fmt == AV_PIX_FMT_ARGB + || pix_fmt == AV_PIX_FMT_RGBA || pix_fmt == AV_PIX_FMT_ABGR || pix_fmt == AV_PIX_FMT_BGRA + || pix_fmt == AV_PIX_FMT_GBRAP) { switch (pix_fmt) { case AV_PIX_FMT_ARGB: case AV_PIX_FMT_RGBA: @@ -2516,6 +2517,7 @@ static void convert_image(producer_avformat self, case mlt_image_yuv422: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: case mlt_image_invalid: break; } diff --git a/src/modules/core/filter_brightness.c b/src/modules/core/filter_brightness.c index 801290e97..d7bbd0da9 100644 --- a/src/modules/core/filter_brightness.c +++ b/src/modules/core/filter_brightness.c @@ -175,6 +175,7 @@ static int filter_get_image(mlt_frame frame, break; case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: *format = mlt_image_rgba; break; case mlt_image_none: diff --git a/src/modules/core/filter_color_transform.c b/src/modules/core/filter_color_transform.c index de7d60a35..6ac20c079 100644 --- a/src/modules/core/filter_color_transform.c +++ b/src/modules/core/filter_color_transform.c @@ -147,6 +147,7 @@ static void ensure_color_properties(mlt_filter self, mlt_frame frame) case mlt_image_rgba64: case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: full_range = 1; break; case mlt_image_yuv422: @@ -182,7 +183,8 @@ static int filter_get_image(mlt_frame frame, mlt_properties filter_properties = MLT_FILTER_PROPERTIES(self); mlt_image_format requested_format = *format; int ret = mlt_frame_get_image(frame, image, format, width, height, writable); - if (ret || requested_format == mlt_image_movit || requested_format == mlt_image_none) + if (ret || requested_format == mlt_image_movit || requested_format == mlt_image_private + || requested_format == mlt_image_none) return ret; const char *out_trc_str = mlt_properties_get(filter_properties, "force_trc"); diff --git a/src/modules/core/loader.ini b/src/modules/core/loader.ini index a753b2e17..4b194c564 100644 --- a/src/modules/core/loader.ini +++ b/src/modules/core/loader.ini @@ -7,7 +7,7 @@ # the second and third are applied as applicable). # image filters -image_convert=movit.convert,avcolor_space,imageconvert +image_convert=placebo.convert,movit.convert,avcolor_space,imageconvert color=color_transform deinterlace=deinterlace,avdeinterlace fieldorder=fieldorder diff --git a/src/modules/core/producer_colour.c b/src/modules/core/producer_colour.c index 1a217eda5..450346f4e 100644 --- a/src/modules/core/producer_colour.c +++ b/src/modules/core/producer_colour.c @@ -94,7 +94,7 @@ static int producer_get_image(mlt_frame frame, *format = mlt_image_format_id(mlt_properties_get(producer_props, "mlt_image_format")); // Choose suitable out values if nothing specific requested - if (*format == mlt_image_none || *format == mlt_image_movit) + if (*format == mlt_image_none || *format == mlt_image_movit || *format == mlt_image_private) *format = mlt_image_rgba; // Optimize the format to avoid unnecessary conversion if ((requested_format == mlt_image_rgba && *format == mlt_image_rgba64) @@ -109,7 +109,7 @@ static int producer_get_image(mlt_frame frame, // Choose default image format if specific request is unsupported if (*format != mlt_image_yuv420p && *format != mlt_image_yuv422 && *format != mlt_image_rgb && *format != mlt_image_movit && *format != mlt_image_opengl_texture - && *format != mlt_image_rgba64) + && *format != mlt_image_private && *format != mlt_image_rgba64) *format = mlt_image_rgba; // See if we need to regenerate @@ -182,6 +182,7 @@ static int producer_get_image(mlt_frame frame, break; case mlt_image_movit: case mlt_image_opengl_texture: + case mlt_image_private: memset(p, 0, size); break; case mlt_image_rgba: diff --git a/src/modules/placebo/CMakeLists.txt b/src/modules/placebo/CMakeLists.txt index 92775505c..82c1fa32d 100644 --- a/src/modules/placebo/CMakeLists.txt +++ b/src/modules/placebo/CMakeLists.txt @@ -7,6 +7,7 @@ endif() add_library(mltplacebo MODULE factory.c gpu_context.c gpu_context.h + filter_placebo_convert.c filter_placebo_render.c filter_placebo_shader.c ) @@ -49,6 +50,7 @@ set_target_properties(mltplacebo PROPERTIES install(TARGETS mltplacebo LIBRARY DESTINATION ${MLT_INSTALL_MODULE_DIR}) install(FILES + filter_placebo_convert.yml 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 index be8c8bde0..47e665071 100644 --- a/src/modules/placebo/factory.c +++ b/src/modules/placebo/factory.c @@ -26,6 +26,10 @@ extern mlt_filter filter_placebo_render_init(mlt_profile profile, mlt_service_type type, const char *id, char *arg); +extern mlt_filter filter_placebo_convert_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, @@ -40,8 +44,13 @@ static mlt_properties metadata(mlt_service_type type, const char *id, void *data MLTPLACEBO_EXPORT MLT_REPOSITORY { + MLT_REGISTER(mlt_service_filter_type, "placebo.convert", filter_placebo_convert_init); 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.convert", + metadata, + "filter_placebo_convert.yml"); MLT_REGISTER_METADATA(mlt_service_filter_type, "placebo.render", metadata, diff --git a/src/modules/placebo/filter_placebo_convert.c b/src/modules/placebo/filter_placebo_convert.c new file mode 100644 index 000000000..50793db5e --- /dev/null +++ b/src/modules/placebo/filter_placebo_convert.c @@ -0,0 +1,174 @@ +/* + * filter_placebo_convert.c -- libplacebo image format converter + * Copyright (C) 2026 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 + +static pl_fmt rgba_texture_format(pl_gpu gpu) +{ + return gpu ? pl_find_named_fmt(gpu, "rgba8") : NULL; +} + +static int upload_rgba(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + int width, + int height, + pl_gpu gpu, + pl_fmt rgba_fmt) +{ + if (*format != mlt_image_rgba) { + if (mlt_frame_next_convert_image(frame, image, format, mlt_image_rgba)) + return 1; + } + if (!*image) + return 1; + + size_t stride = width * 4; + pl_tex tex = NULL; + + placebo_render_lock(); + tex = pl_tex_create(gpu, + pl_tex_params(.w = width, + .h = height, + .format = rgba_fmt, + .sampleable = true, + .renderable = true, + .host_readable = true, + .host_writable = true, )); + if (!tex) { + placebo_render_unlock(); + return 1; + } + if (!pl_tex_upload(gpu, + pl_tex_transfer_params(.tex = tex, .row_pitch = stride, .ptr = *image, ))) { + pl_tex_destroy(gpu, &tex); + placebo_render_unlock(); + return 1; + } + placebo_render_unlock(); + + *format = mlt_image_private; + *image = (uint8_t *) tex; + if (placebo_frame_set_tex(frame, tex)) { + placebo_render_lock(); + pl_tex_destroy(gpu, &tex); + placebo_render_unlock(); + *image = NULL; + return 1; + } + return 0; +} + +static int download_rgba(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output_format, + int width, + int height, + pl_gpu gpu) +{ + pl_tex tex = placebo_image_get_tex(*image); + if (!tex) + return 1; + + int size = mlt_image_format_size(mlt_image_rgba, width, height, NULL); + uint8_t *output = mlt_pool_alloc(size); + if (!output) + return 1; + + placebo_render_lock(); + int ok = pl_tex_download(gpu, + pl_tex_transfer_params(.tex = tex, + .row_pitch = width * 4, + .ptr = output, )); + placebo_render_unlock(); + if (!ok) { + mlt_pool_release(output); + return 1; + } + + *image = output; + *format = mlt_image_rgba; + mlt_properties_set(MLT_FRAME_PROPERTIES(frame), "mlt_image_format", NULL); + if (mlt_frame_set_image(frame, output, size, mlt_pool_release)) { + mlt_pool_release(output); + *image = NULL; + return 1; + } + if (output_format != mlt_image_rgba) + return mlt_frame_next_convert_image(frame, image, format, output_format); + return 0; +} + +static int convert_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output_format) +{ + if (*format == output_format && *format != mlt_image_private) + return 0; + + if (placebo_frame_is_tex(frame, *format) && placebo_frame_wants_tex(frame, output_format)) + return 0; + + pl_gpu gpu = placebo_gpu_get(); + pl_fmt rgba_fmt = rgba_texture_format(gpu); + mlt_properties props = MLT_FRAME_PROPERTIES(frame); + int width = mlt_properties_get_int(props, "width"); + int height = mlt_properties_get_int(props, "height"); + + if (!gpu || !rgba_fmt || !image || width < 1 || height < 1) + return 1; + + mlt_log_verbose(NULL, + "[placebo.convert] Converting from format %d to %d\n", + *format, + output_format); + if (placebo_frame_wants_tex(frame, output_format)) + return upload_rgba(frame, image, format, width, height, gpu, rgba_fmt); + + if (placebo_frame_is_tex(frame, *format)) + return download_rgba(frame, image, format, output_format, width, height, gpu); + + return 1; +} + +static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) +{ + mlt_frame_push_convert_image(frame, convert_image); + return frame; +} + +mlt_filter filter_placebo_convert_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; + return filter; +} \ No newline at end of file diff --git a/src/modules/placebo/filter_placebo_convert.yml b/src/modules/placebo/filter_placebo_convert.yml new file mode 100644 index 000000000..5bda5c7cf --- /dev/null +++ b/src/modules/placebo/filter_placebo_convert.yml @@ -0,0 +1,14 @@ +schema_version: 7.0 +type: filter +identifier: placebo.convert +title: GPU Format Converter (libplacebo) +version: 1 +copyright: Copyright (C) 2026 D-Ogi +creator: D-Ogi +license: LGPLv2.1 +language: en +tags: + - Video +description: > + Converts frame images between CPU pixel formats and the internal + libplacebo GPU texture format used by placebo.render and placebo.shader. \ No newline at end of file diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index 1e859fd00..f1cee45c6 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -106,53 +106,31 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* Request RGBA from upstream */ - *format = mlt_image_rgba; + /* Request placebo from upstream. placebo.convert is responsible for any + * CPU upload/download around non-placebo filters in the chain. */ + placebo_frame_set_requested_tex(frame, 1); + *format = mlt_image_private; int error = mlt_frame_get_image(frame, image, format, width, height, 1); + placebo_frame_set_requested_tex(frame, 0); 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; - } + if (!placebo_frame_is_tex(frame, *format)) { + mlt_log_error(MLT_FILTER_SERVICE(filter), + "Expected placebo private input, got %s\n", + mlt_image_format_name(*format)); + return 1; } + pl_tex src_tex = placebo_image_get_tex(*image); + if (!src_tex) + return 1; /* sampleable is needed so a subsequent placebo filter can bind this * texture as its source without re-uploading from RAM. */ + placebo_render_lock(); pl_tex dst_tex = pl_tex_create(gpu, pl_tex_params(.w = w, .h = h, @@ -164,7 +142,7 @@ static int filter_get_image(mlt_frame frame, mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create dest texture\n"); pl_tex_destroy(gpu, &src_tex); placebo_render_unlock(); - return 0; + return 1; } /* Build source and target pl_frames */ @@ -238,23 +216,17 @@ static int filter_get_image(mlt_frame frame, 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(); + *format = mlt_image_private; + *image = (uint8_t *) dst_tex; + if (placebo_frame_set_tex(frame, dst_tex)) { + placebo_render_lock(); + pl_tex_destroy(gpu, &dst_tex); + placebo_render_unlock(); + return 1; + } + return 0; } diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index a32131180..bc31ea0f8 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -354,53 +354,33 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* Request RGBA from upstream */ - *format = mlt_image_rgba; + /* Request placebo from upstream. placebo.convert is responsible for any + * CPU upload/download around non-placebo filters in the chain. */ + placebo_frame_set_requested_tex(frame, 1); + *format = mlt_image_private; int error = mlt_frame_get_image(frame, image, format, width, height, 1); + placebo_frame_set_requested_tex(frame, 0); if (error || !*image) return error; + mlt_log_verbose(NULL, "[placebo.shader] Got image from upstream: format=%d\n", *format); + 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; - } + if (!placebo_frame_is_tex(frame, *format)) { + mlt_log_error(MLT_FILTER_SERVICE(filter), + "Expected placebo private input, got %s\n", + mlt_image_format_name(*format)); + return 1; } + pl_tex src_tex = placebo_image_get_tex(*image); + if (!src_tex) + return 1; /* sampleable is needed so a subsequent placebo filter can bind this * texture as its source without re-uploading from RAM. */ + placebo_render_lock(); pl_tex dst_tex = pl_tex_create(gpu, pl_tex_params(.w = w, .h = h, @@ -412,7 +392,7 @@ static int filter_get_image(mlt_frame frame, mlt_log_error(MLT_FILTER_SERVICE(filter), "Failed to create dest texture\n"); pl_tex_destroy(gpu, &src_tex); placebo_render_unlock(); - return 0; + return 1; } /* Build pl_frames */ @@ -454,23 +434,17 @@ static int filter_get_image(mlt_frame frame, 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(); + *format = mlt_image_private; + *image = (uint8_t *) dst_tex; + if (placebo_frame_set_tex(frame, dst_tex)) { + placebo_render_lock(); + pl_tex_destroy(gpu, &dst_tex); + placebo_render_unlock(); + return 1; + } + return 0; } diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index c32cfad97..6f9e03c37 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -19,6 +19,8 @@ #include "gpu_context.h" +#include + #include #include #include @@ -374,82 +376,57 @@ 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. - */ +static void placebo_tex_destroy(void *ptr) +{ + pl_tex tex = (pl_tex) ptr; + if (!tex) + return; + pl_gpu gpu = placebo_gpu_get(); + if (!gpu) + return; + placebo_render_lock(); + pl_tex_destroy(gpu, &tex); + placebo_render_unlock(); +} -typedef struct +pl_tex placebo_image_get_tex(const uint8_t *image) { - 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) + return (pl_tex) image; +} + +void placebo_frame_set_requested_tex(mlt_frame frame, int requested) { - 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(); - } + mlt_properties props = MLT_FRAME_PROPERTIES(frame); + int count = mlt_properties_get_int(props, "_placebo_requested"); + + if (requested) { + count++; + } else if (count > 0) { + count--; } - free(d); + + mlt_properties_set_int(props, "_placebo_requested", count); } -pl_tex placebo_frame_take_tex(mlt_frame frame, const uint8_t *current_image) +int placebo_frame_wants_tex(mlt_frame frame, mlt_image_format format) { - 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; + return format == mlt_image_private + && mlt_properties_get_int(MLT_FRAME_PROPERTIES(frame), "_placebo_requested") > 0; +} + +int placebo_frame_is_tex(mlt_frame frame, mlt_image_format format) +{ + const char *image_format = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), "mlt_image_format"); + return format == mlt_image_private && image_format && !strcmp(image_format, "placebo"); } -void placebo_frame_put_tex(mlt_frame frame, pl_tex tex, const uint8_t *image_ptr) +int placebo_frame_set_tex(mlt_frame frame, pl_tex tex) { - 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); + mlt_properties props = MLT_FRAME_PROPERTIES(frame); + mlt_properties_set_int(props, "format", mlt_image_private); + mlt_properties_set(props, "mlt_image_format", "placebo"); + mlt_properties_set_data(props, "alpha", NULL, 0, NULL, NULL); + return mlt_frame_set_image(frame, (uint8_t *) tex, 0, placebo_tex_destroy); } /* ---------- GPU teardown ---------- */ diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 3bf2818fb..3a921b287 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -46,16 +46,21 @@ 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); +/* Interpret an opaque placebo image payload as a libplacebo texture handle. */ +pl_tex placebo_image_get_tex(const uint8_t *image); + +/* Mark or clear a request for a placebo private image during an upstream pull. + Nested requests on the same frame are reference-counted. */ +void placebo_frame_set_requested_tex(mlt_frame frame, int requested); + +/* Return non-zero when the frame currently requests a placebo private image. */ +int placebo_frame_wants_tex(mlt_frame frame, mlt_image_format format); + +/* Return non-zero when the frame currently stores a placebo private image. */ +int placebo_frame_is_tex(mlt_frame frame, mlt_image_format format); + +/* Store a libplacebo texture on the frame as mlt_image_private. + The texture lifetime is owned by the frame image property. */ +int placebo_frame_set_tex(mlt_frame frame, pl_tex tex); #endif /* GPU_CONTEXT_H */ From 1def44bd4b0a58454c7559776658f1ae6c8b0cf7 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 09:54:02 -0700 Subject: [PATCH 09/24] fix cppcheck and redundant workflow runs --- .github/workflows/build-distros.yml | 3 +++ .github/workflows/build-linux.yml | 10 +++++++++- .github/workflows/build-msys2-mingw64.yml | 11 ++++++++++- .github/workflows/build-windows-msvc.yml | 12 ++++++++++-- .github/workflows/static-code-analysis.yml | 11 ++++++++++- makefile | 6 +++++- 6 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-distros.yml b/.github/workflows/build-distros.yml index cc367402e..7b36cbd8c 100644 --- a/.github/workflows/build-distros.yml +++ b/.github/workflows/build-distros.yml @@ -20,6 +20,9 @@ on: - debian-stable - fedora-42 - fedora-38 +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 892ff4165..4e9b73721 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -1,6 +1,14 @@ name: build-test-linux-on-push -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: python: diff --git a/.github/workflows/build-msys2-mingw64.yml b/.github/workflows/build-msys2-mingw64.yml index 28dca5893..78961248d 100644 --- a/.github/workflows/build-msys2-mingw64.yml +++ b/.github/workflows/build-msys2-mingw64.yml @@ -1,6 +1,15 @@ name: build-test-msys2-mingw64-on-push -on: [push, pull_request, workflow_dispatch] +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: build: diff --git a/.github/workflows/build-windows-msvc.yml b/.github/workflows/build-windows-msvc.yml index ddc47c3d3..83edfa523 100644 --- a/.github/workflows/build-windows-msvc.yml +++ b/.github/workflows/build-windows-msvc.yml @@ -1,6 +1,14 @@ name: build-test-windows-msvc-on-push -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true permissions: packages: write @@ -55,4 +63,4 @@ jobs: - name: Run tests run: | - ctest --test-dir build -C Debug + ctest --test-dir build -C Debug --rerun-failed --output-on-failure diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml index f1de06aaf..57cc3622e 100644 --- a/.github/workflows/static-code-analysis.yml +++ b/.github/workflows/static-code-analysis.yml @@ -1,6 +1,14 @@ name: static-code-analysis -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: cppcheck: @@ -19,6 +27,7 @@ jobs: --library=cppcheck.cfg --suppress=ctuOneDefinitionRuleViolation --suppress=syntaxError:src/modules/xml/common.c + --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 diff --git a/makefile b/makefile index d16b0a056..5abbec01a 100644 --- a/makefile +++ b/makefile @@ -29,4 +29,8 @@ cppcheck: --include=src/framework/mlt_types.h \ --library=cppcheck.cfg \ --suppress=ctuOneDefinitionRuleViolation \ - --suppress=syntaxError:src/modules/xml/common.c + --suppress=syntaxError:src/modules/xml/common.c \ + --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 From e80a020bfa21daf9f2775ff2facd1cbf8eaf912c Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 10:17:28 -0700 Subject: [PATCH 10/24] fix ctest on msvc --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 89bc70bc4..b26ab676f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,7 +100,7 @@ endif() if(NOT EXISTS ${MLT_DATA_OUTPUT_DIRECTORY}) if(WIN32) # symlinks require admin rights on Windows - file(COPY "${CMAKE_SOURCE_DIR}/src/modules" DESTINATION "${CMAKE_BINARY_DIR}/out/share" FILES_MATCHING REGEX yml|txt) + file(COPY "${CMAKE_SOURCE_DIR}/src/modules" DESTINATION "${CMAKE_BINARY_DIR}/out/share" FILES_MATCHING REGEX "yml|txt|ini|dict") file(RENAME "${CMAKE_BINARY_DIR}/out/share/modules" "${MLT_DATA_OUTPUT_DIRECTORY}") file(COPY "${CMAKE_SOURCE_DIR}/presets" DESTINATION "${MLT_DATA_OUTPUT_DIRECTORY}") file(COPY "${CMAKE_SOURCE_DIR}/profiles" DESTINATION "${MLT_DATA_OUTPUT_DIRECTORY}") From 626cf8acd791c6287d710e7b35bbb41316bdb29d Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 11:03:28 -0700 Subject: [PATCH 11/24] docs for placebo private image handling --- src/modules/placebo/gpu_context.c | 10 ++++++++++ src/modules/placebo/gpu_context.h | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 6f9e03c37..f9ad303c9 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -394,6 +394,16 @@ pl_tex placebo_image_get_tex(const uint8_t *image) return (pl_tex) image; } +/* ---------- mlt_image_private helpers ---------- */ + +/* Request state and result state must not share one property: + * - requests are transient and may nest while multiple placebo filters pull + * upstream on the same frame; + * - results describe the frame's current image payload and must only be set + * when that payload really is a placebo texture. + * If these are conflated, one filter can clear another filter's in-flight + * request or a non-placebo result can be mistaken for a valid private texture. */ + void placebo_frame_set_requested_tex(mlt_frame frame, int requested) { mlt_properties props = MLT_FRAME_PROPERTIES(frame); diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 3a921b287..7541f2148 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -50,13 +50,20 @@ void placebo_gpu_release(void); pl_tex placebo_image_get_tex(const uint8_t *image); /* Mark or clear a request for a placebo private image during an upstream pull. - Nested requests on the same frame are reference-counted. */ + This is intentionally separate from the actual output marker used by + placebo_frame_is_tex()/placebo_frame_set_tex(). A request means "a downstream + placebo filter wants private GPU data if a converter can provide it"; it does + not mean the frame currently contains such data. Nested requests on the same + frame are reference-counted because multiple placebo filters can be stacked + during one pull. */ void placebo_frame_set_requested_tex(mlt_frame frame, int requested); -/* Return non-zero when the frame currently requests a placebo private image. */ +/* Return non-zero when the current conversion target is a placebo private image + request, not merely any mlt_image_private use by some other module. */ int placebo_frame_wants_tex(mlt_frame frame, mlt_image_format format); -/* Return non-zero when the frame currently stores a placebo private image. */ +/* Return non-zero when the frame currently stores a placebo private image. + This checks the actual image payload marker, not the transient request state. */ int placebo_frame_is_tex(mlt_frame frame, mlt_image_format format); /* Store a libplacebo texture on the frame as mlt_image_private. From 9f97a830d01f10f9a82e4db9afafca3e8a16ac61 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 13:31:08 -0700 Subject: [PATCH 12/24] Enhance image conversion logic to handle private format cases --- src/modules/placebo/filter_placebo_convert.c | 14 ++++++++++---- src/modules/placebo/filter_placebo_shader.c | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/modules/placebo/filter_placebo_convert.c b/src/modules/placebo/filter_placebo_convert.c index 50793db5e..3662b4e15 100644 --- a/src/modules/placebo/filter_placebo_convert.c +++ b/src/modules/placebo/filter_placebo_convert.c @@ -128,9 +128,15 @@ static int convert_image(mlt_frame frame, mlt_image_format *format, mlt_image_format output_format) { + // Succeed quickly if there is nothing for any converter to do if (*format == output_format && *format != mlt_image_private) return 0; + // Fail fast if not converting to or from mlt_image_private + if (*format != mlt_image_private && output_format != mlt_image_private) + return 1; + + // Reuse existing GPU texture if (placebo_frame_is_tex(frame, *format) && placebo_frame_wants_tex(frame, output_format)) return 0; @@ -143,10 +149,10 @@ static int convert_image(mlt_frame frame, if (!gpu || !rgba_fmt || !image || width < 1 || height < 1) return 1; - mlt_log_verbose(NULL, - "[placebo.convert] Converting from format %d to %d\n", - *format, - output_format); + mlt_log_debug(NULL, + "[placebo.convert] Converting from format %d to %d\n", + *format, + output_format); if (placebo_frame_wants_tex(frame, output_format)) return upload_rgba(frame, image, format, width, height, gpu, rgba_fmt); diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index bc31ea0f8..11228f6cc 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -363,7 +363,7 @@ static int filter_get_image(mlt_frame frame, if (error || !*image) return error; - mlt_log_verbose(NULL, "[placebo.shader] Got image from upstream: format=%d\n", *format); + mlt_log_debug(NULL, "[placebo.shader] Got image from upstream: format=%d\n", *format); int w = *width; int h = *height; From 739224f27a59882ce7bb8b6aebd9d0c112a1d27d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 6 May 2026 20:51:10 +0000 Subject: [PATCH 13/24] Fix double-free of src_tex and out-of-bounds in imageconvert - Remove incorrect pl_tex_destroy(src_tex) in error paths of the render and shader filters: src_tex is owned by the frame and freed automatically when placebo_frame_set_tex() replaces the "image" property, so manual destruction caused a double-free on frame close. - Add *image = NULL after failed placebo_frame_set_tex() in render and shader filters (consistent with filter_placebo_convert.c). - Guard filter_imageconvert's conversion_matrix access: mlt_image_private (and mlt_image_invalid) are out of range for the matrix indexed by [format - 1]; early-return 1 for any format outside [mlt_image_rgb, mlt_image_rgba64] to avoid an out-of-bounds read. Agent-Logs-Url: https://github.com/mltframework/mlt/sessions/f71fd929-5b62-4e71-864a-13291a3ec28e Co-authored-by: ddennedy <1146683+ddennedy@users.noreply.github.com> --- src/modules/core/filter_imageconvert.c | 4 ++++ src/modules/placebo/filter_placebo_render.c | 2 +- src/modules/placebo/filter_placebo_shader.c | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/modules/core/filter_imageconvert.c b/src/modules/core/filter_imageconvert.c index 4f5ed5d28..a60571c4a 100644 --- a/src/modules/core/filter_imageconvert.c +++ b/src/modules/core/filter_imageconvert.c @@ -447,6 +447,10 @@ static int convert_image(mlt_frame frame, int height = mlt_properties_get_int(properties, "height"); if (*format != requested_format) { + // Only handle standard CPU formats; skip private/invalid/none formats + if (*format <= mlt_image_none || *format >= mlt_image_invalid + || requested_format <= mlt_image_none || requested_format >= mlt_image_invalid) + return 1; conversion_function converter = conversion_matrix[*format - 1][requested_format - 1]; mlt_log_debug(NULL, diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index f1cee45c6..e2bdca98c 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -140,7 +140,6 @@ static int filter_get_image(mlt_frame frame, .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 1; } @@ -224,6 +223,7 @@ static int filter_get_image(mlt_frame frame, placebo_render_lock(); pl_tex_destroy(gpu, &dst_tex); placebo_render_unlock(); + *image = NULL; return 1; } diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index 11228f6cc..a0bc187f5 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -390,7 +390,6 @@ static int filter_get_image(mlt_frame frame, .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 1; } @@ -442,6 +441,7 @@ static int filter_get_image(mlt_frame frame, placebo_render_lock(); pl_tex_destroy(gpu, &dst_tex); placebo_render_unlock(); + *image = NULL; return 1; } From 86df189c2d84f24f06d838d31b1689f280d307f8 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 14:16:55 -0700 Subject: [PATCH 14/24] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- CMakeLists.txt | 2 -- makefile | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index b26ab676f..e1582ab11 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,7 +34,6 @@ option(MOD_NORMALIZE "Enable Normalize module (GPL)" ON) option(MOD_OLDFILM "Enable Oldfilm module" ON) option(MOD_OPENCV "Enable OpenCV module" OFF) option(MOD_OPENFX "Enable OpenFX module (GPL)" ON) -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) @@ -594,7 +593,6 @@ 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: OpenFX" MOD_OPENFX "") -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 "") diff --git a/makefile b/makefile index 5abbec01a..b6af992c5 100644 --- a/makefile +++ b/makefile @@ -30,7 +30,7 @@ cppcheck: --library=cppcheck.cfg \ --suppress=ctuOneDefinitionRuleViolation \ --suppress=syntaxError:src/modules/xml/common.c \ - --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 + --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 From 7cd956a73aa57638c56f3b582e0aa4a52a95577b Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 14:29:16 -0700 Subject: [PATCH 15/24] fix lazy allocation not thread-safe --- src/modules/placebo/filter_placebo_shader.c | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index a0bc187f5..440b69f33 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -339,14 +339,10 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* Lazily allocate private data; destruction is handled by mlt_properties */ + /* Private data allocated during init; just read it here (safe). */ 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); - } + if (!priv) + return mlt_frame_get_image(frame, image, format, width, height, writable); /* Ensure shader is loaded */ if (!ensure_shader(filter, priv, gpu)) { @@ -466,6 +462,12 @@ mlt_filter filter_placebo_shader_init(mlt_profile profile, mlt_properties props = MLT_FILTER_PROPERTIES(filter); mlt_properties_set(props, "shader_path", arg ? arg : ""); mlt_properties_set(props, "shader_text", ""); + shader_private *priv = calloc(1, sizeof(shader_private)); + if (!priv) { + mlt_filter_close(filter); + return NULL; + } + mlt_properties_set_data(props, "_shader_priv", priv, 0, shader_private_destroy, NULL); } return filter; } From 9a6e13418b7070d5df35198cf8e04f77399d2db6 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 14:39:04 -0700 Subject: [PATCH 16/24] fix races in placebo.shader --- src/modules/placebo/filter_placebo_shader.c | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index 440b69f33..6ab803abf 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -344,14 +344,16 @@ static int filter_get_image(mlt_frame frame, if (!priv) return mlt_frame_get_image(frame, image, format, width, height, writable); - /* Ensure shader is loaded */ - if (!ensure_shader(filter, priv, gpu)) { - /* No shader available -- pass through */ + /* Fast path: skip the GPU pipeline entirely when no shader is configured. + * Property reads are safe here; we only mutate priv under placebo_render_lock. */ + const char *shader_path = mlt_properties_get(filter_props, "shader_path"); + const char *shader_text = mlt_properties_get(filter_props, "shader_text"); + if ((!shader_path || !*shader_path) && (!shader_text || !*shader_text)) return mlt_frame_get_image(frame, image, format, width, height, writable); - } - /* Request placebo from upstream. placebo.convert is responsible for any - * CPU upload/download around non-placebo filters in the chain. */ + /* Fetch the upstream image before acquiring the render lock so the + * potentially-expensive upstream pipeline runs concurrently with other + * frames that are already inside the lock. */ placebo_frame_set_requested_tex(frame, 1); *format = mlt_image_private; int error = mlt_frame_get_image(frame, image, format, width, height, 1); @@ -374,9 +376,21 @@ static int filter_get_image(mlt_frame frame, if (!src_tex) return 1; + /* Hold the render lock for the entire shader-reload + render sequence. + * Without this, concurrent worker threads can race on priv->hooks: + * one thread destroys and replaces hooks[0] while another thread is + * still using it in pl_render_image (use-after-free / double-destroy). */ + placebo_render_lock(); + + /* Reload shader if path/text/mtime changed; returns 0 if unavailable. */ + if (!ensure_shader(filter, priv, gpu)) { + placebo_render_unlock(); + /* src_tex is already attached to the frame; pass it through. */ + return 0; + } + /* sampleable is needed so a subsequent placebo filter can bind this * texture as its source without re-uploading from RAM. */ - placebo_render_lock(); pl_tex dst_tex = pl_tex_create(gpu, pl_tex_params(.w = w, .h = h, From f2a9e83c01c2fa241c6e25307524c956bab842e7 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 14:45:01 -0700 Subject: [PATCH 17/24] Add permanent failure flag for GPU initialization and fix leak --- src/modules/placebo/gpu_context.c | 43 +++++++++++++++++++------------ src/modules/placebo/gpu_context.h | 2 +- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index f9ad303c9..c1c1fc015 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -83,6 +83,8 @@ static pl_renderer s_renderer = NULL; static pl_cache s_cache = NULL; static int s_initialized = 0; +static int s_gpu_failed = 0; +static void release_gpu_locked(void); /* ---------- Mutex (SRWLOCK on Windows is statically initializable, unlike CRITICAL_SECTION) ---------- */ @@ -130,7 +132,7 @@ static void get_cache_path(char *buf, size_t len) static void load_cache(void) { - char path[512]; + char path[PATH_MAX]; get_cache_path(path, sizeof(path)); if (path[0] == '\0') return; @@ -308,10 +310,10 @@ static int init_gpu(void) return 0; done: - if (s_cache) + if (s_cache) { pl_gpu_set_cache(s_gpu, s_cache); - - load_cache(); + load_cache(); + } mlt_factory_register_for_clean_up(NULL, (mlt_destructor) placebo_gpu_release); @@ -323,13 +325,16 @@ static int init_gpu(void) pl_gpu placebo_gpu_get(void) { LOCK(); - if (!s_initialized) { - s_initialized = 1; + if (!s_initialized && !s_gpu_failed) { if (!init_gpu()) { - /* Stay initialized=1 to prevent retry spam on every frame */ + /* Free any partial resources (pl_log, cache, loader handles) before + * setting the permanent failure flag to suppress future retries. */ + release_gpu_locked(); + s_gpu_failed = 1; UNLOCK(); return NULL; } + s_initialized = 1; } pl_gpu result = s_gpu; UNLOCK(); @@ -441,16 +446,9 @@ int placebo_frame_set_tex(mlt_frame frame, pl_tex tex) /* ---------- GPU teardown ---------- */ -void placebo_gpu_release(void) +/* Internal: free all GPU resources. Caller must hold s_mutex. */ +static void release_gpu_locked(void) { - LOCK(); - if (!s_initialized) { - UNLOCK(); - return; - } - - save_cache(); - if (s_renderer) { pl_renderer_destroy(&s_renderer); s_renderer = NULL; @@ -503,6 +501,19 @@ void placebo_gpu_release(void) s_gpu = NULL; s_initialized = 0; + s_gpu_failed = 0; +} + +void placebo_gpu_release(void) +{ + LOCK(); + if (!s_initialized) { + UNLOCK(); + return; + } + + save_cache(); + release_gpu_locked(); UNLOCK(); } diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 7541f2148..4a14b2d36 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -43,7 +43,7 @@ pl_renderer placebo_renderer_get(void); void placebo_render_lock(void); void placebo_render_unlock(void); -/* Tear down all GPU resources. Registered via atexit on first init. */ +/* Tear down all GPU resources. Registered for cleanup on first init. */ void placebo_gpu_release(void); /* Interpret an opaque placebo image payload as a libplacebo texture handle. */ From 8d7260a0827a96fa75eedb4e17ae0fa845b98b35 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 15:06:28 -0700 Subject: [PATCH 18/24] Add permanent failure and shutdown flags for GPU management --- src/modules/placebo/gpu_context.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index c1c1fc015..420158a71 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -83,7 +83,8 @@ static pl_renderer s_renderer = NULL; static pl_cache s_cache = NULL; static int s_initialized = 0; -static int s_gpu_failed = 0; +static int s_gpu_failed = 0; /* permanent failure flag; suppresses retries */ +static int s_gpu_shutdown = 0; /* set during placebo_gpu_release(); prevents reinit */ static void release_gpu_locked(void); /* ---------- Mutex (SRWLOCK on Windows is statically initializable, unlike CRITICAL_SECTION) ---------- */ @@ -325,7 +326,7 @@ static int init_gpu(void) pl_gpu placebo_gpu_get(void) { LOCK(); - if (!s_initialized && !s_gpu_failed) { + if (!s_initialized && !s_gpu_failed && !s_gpu_shutdown) { if (!init_gpu()) { /* Free any partial resources (pl_log, cache, loader handles) before * setting the permanent failure flag to suppress future retries. */ @@ -502,6 +503,7 @@ static void release_gpu_locked(void) s_gpu = NULL; s_initialized = 0; s_gpu_failed = 0; + s_gpu_shutdown = 0; } void placebo_gpu_release(void) @@ -512,6 +514,7 @@ void placebo_gpu_release(void) return; } + s_gpu_shutdown = 1; /* block any reinit triggered by in-flight tex destructors */ save_cache(); release_gpu_locked(); From 34b271797fbefa64ef98dcd831bfceb9e24c83b6 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Wed, 6 May 2026 21:49:49 -0700 Subject: [PATCH 19/24] use "mlt_image_private" and mlt_properties_clear() --- src/modules/placebo/gpu_context.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 420158a71..79a7ef22f 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -432,7 +432,7 @@ int placebo_frame_wants_tex(mlt_frame frame, mlt_image_format format) int placebo_frame_is_tex(mlt_frame frame, mlt_image_format format) { - const char *image_format = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), "mlt_image_format"); + const char *image_format = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), "mlt_image_private"); return format == mlt_image_private && image_format && !strcmp(image_format, "placebo"); } @@ -440,8 +440,8 @@ int placebo_frame_set_tex(mlt_frame frame, pl_tex tex) { mlt_properties props = MLT_FRAME_PROPERTIES(frame); mlt_properties_set_int(props, "format", mlt_image_private); - mlt_properties_set(props, "mlt_image_format", "placebo"); - mlt_properties_set_data(props, "alpha", NULL, 0, NULL, NULL); + mlt_properties_set(props, "mlt_image_private", "placebo"); + mlt_properties_clear(props, "alpha"); return mlt_frame_set_image(frame, (uint8_t *) tex, 0, placebo_tex_destroy); } From ef2da1c0c963ed4869d5f73c1ed1d4a220f5852b Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 13:58:38 -0700 Subject: [PATCH 20/24] add `mlt_frame_prepend_convert_image()` --- NEWS | 1 + src/framework/mlt.vers | 1 + src/framework/mlt_frame.c | 33 +++++++++++++++++++++++++++++++++ src/framework/mlt_frame.h | 1 + 4 files changed, 36 insertions(+) diff --git a/NEWS b/NEWS index b100ad20c..56e6c1cc2 100644 --- a/NEWS +++ b/NEWS @@ -8,6 +8,7 @@ Framework Use `mlt_frame_push_convert_image()` to register a converter instead of setting `frame->convert_image` directly. Multiple converters are tried in registration order; the first that succeeds wins. New public API: + - `mlt_frame_prepend_convert_image()` - `mlt_frame_push_convert_image()` - `mlt_frame_convert_image()` - `mlt_frame_next_convert_image()` diff --git a/src/framework/mlt.vers b/src/framework/mlt.vers index 63622f72f..23c2f012e 100644 --- a/src/framework/mlt.vers +++ b/src/framework/mlt.vers @@ -691,6 +691,7 @@ MLT_7.36.0 { MLT_7.40.0 { global: mlt_frame_push_convert_image; + mlt_frame_prepend_convert_image; mlt_frame_has_convert_image; mlt_frame_convert_image; mlt_frame_next_convert_image; diff --git a/src/framework/mlt_frame.c b/src/framework/mlt_frame.c index f07749f6f..aef69f580 100644 --- a/src/framework/mlt_frame.c +++ b/src/framework/mlt_frame.c @@ -890,6 +890,39 @@ void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert) mlt_deque_push_back(list, (void *) convert); } +/** Register an image-conversion callback on the frame. + * + * Callbacks are prepended to a list and dispatched in order by + * \p mlt_frame_convert_image. If \p convert is already registered on this + * frame it is silently ignored (deduplication). Has no effect if either + * argument is NULL. + * + * \public \memberof mlt_frame_s + * \param self a frame + * \param convert the conversion callback to register + */ +void mlt_frame_prepend_convert_image(mlt_frame self, mlt_convert_image convert) +{ + if (!self || !convert) + return; + mlt_properties props = MLT_FRAME_PROPERTIES(self); + mlt_deque list = mlt_properties_get_data(props, CONVERT_IMAGE_CALLBACKS, NULL); + if (!list) { + list = mlt_deque_init(); + mlt_properties_set_data(props, + CONVERT_IMAGE_CALLBACKS, + list, + 0, + (mlt_destructor) mlt_deque_close, + NULL); + } + // Deduplicate: don't register the same function pointer twice. + for (int i = 0; i < mlt_deque_count(list); i++) + if (mlt_deque_peek(list, i) == (void *) convert) + return; + mlt_deque_push_front(list, (void *) convert); +} + /** Determine whether any image-conversion callbacks are registered. * * \public \memberof mlt_frame_s diff --git a/src/framework/mlt_frame.h b/src/framework/mlt_frame.h index 889b2130b..a1fd10132 100644 --- a/src/framework/mlt_frame.h +++ b/src/framework/mlt_frame.h @@ -184,6 +184,7 @@ MLT_EXPORT void mlt_frame_close(mlt_frame self); MLT_EXPORT mlt_properties mlt_frame_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT mlt_properties mlt_frame_get_unique_properties(mlt_frame self, mlt_service service); MLT_EXPORT void mlt_frame_push_convert_image(mlt_frame self, mlt_convert_image convert); +MLT_EXPORT void mlt_frame_prepend_convert_image(mlt_frame self, mlt_convert_image convert); MLT_EXPORT int mlt_frame_has_convert_image(mlt_frame self); MLT_EXPORT int mlt_frame_convert_image(mlt_frame self, uint8_t **image, From 6c75bd140015582c9af96812ceaa9e92b9118cd6 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 14:32:16 -0700 Subject: [PATCH 21/24] remove placebo.convert filter prepend the callback on the frame as-needed --- src/modules/core/loader.ini | 2 +- src/modules/placebo/CMakeLists.txt | 5 ++- .../{filter_placebo_convert.c => convert.c} | 33 +++++-------------- src/modules/placebo/convert.h | 30 +++++++++++++++++ src/modules/placebo/factory.c | 9 ----- .../placebo/filter_placebo_convert.yml | 14 -------- src/modules/placebo/filter_placebo_render.c | 7 ++-- src/modules/placebo/filter_placebo_shader.c | 7 ++-- src/modules/placebo/gpu_context.c | 5 +-- src/modules/placebo/gpu_context.h | 2 ++ 10 files changed, 56 insertions(+), 58 deletions(-) rename src/modules/placebo/{filter_placebo_convert.c => convert.c} (85%) create mode 100644 src/modules/placebo/convert.h delete mode 100644 src/modules/placebo/filter_placebo_convert.yml diff --git a/src/modules/core/loader.ini b/src/modules/core/loader.ini index 4b194c564..a753b2e17 100644 --- a/src/modules/core/loader.ini +++ b/src/modules/core/loader.ini @@ -7,7 +7,7 @@ # the second and third are applied as applicable). # image filters -image_convert=placebo.convert,movit.convert,avcolor_space,imageconvert +image_convert=movit.convert,avcolor_space,imageconvert color=color_transform deinterlace=deinterlace,avdeinterlace fieldorder=fieldorder diff --git a/src/modules/placebo/CMakeLists.txt b/src/modules/placebo/CMakeLists.txt index 82c1fa32d..480890d6e 100644 --- a/src/modules/placebo/CMakeLists.txt +++ b/src/modules/placebo/CMakeLists.txt @@ -5,11 +5,11 @@ if(NOT libplacebo_FOUND) endif() add_library(mltplacebo MODULE + convert.c convert.h factory.c - gpu_context.c gpu_context.h - filter_placebo_convert.c filter_placebo_render.c filter_placebo_shader.c + gpu_context.c gpu_context.h ) file(GLOB YML "*.yml") @@ -50,7 +50,6 @@ set_target_properties(mltplacebo PROPERTIES install(TARGETS mltplacebo LIBRARY DESTINATION ${MLT_INSTALL_MODULE_DIR}) install(FILES - filter_placebo_convert.yml filter_placebo_render.yml filter_placebo_shader.yml DESTINATION ${MLT_INSTALL_DATA_DIR}/placebo diff --git a/src/modules/placebo/filter_placebo_convert.c b/src/modules/placebo/convert.c similarity index 85% rename from src/modules/placebo/filter_placebo_convert.c rename to src/modules/placebo/convert.c index 3662b4e15..6bf844aea 100644 --- a/src/modules/placebo/filter_placebo_convert.c +++ b/src/modules/placebo/convert.c @@ -1,5 +1,5 @@ /* - * filter_placebo_convert.c -- libplacebo image format converter + * convert.c -- libplacebo image format converter * Copyright (C) 2026 D-Ogi * * This library is free software; you can redistribute it and/or @@ -123,10 +123,10 @@ static int download_rgba(mlt_frame frame, return 0; } -static int convert_image(mlt_frame frame, - uint8_t **image, - mlt_image_format *format, - mlt_image_format output_format) +int placebo_convert_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output_format) { // Succeed quickly if there is nothing for any converter to do if (*format == output_format && *format != mlt_image_private) @@ -150,9 +150,9 @@ static int convert_image(mlt_frame frame, return 1; mlt_log_debug(NULL, - "[placebo.convert] Converting from format %d to %d\n", - *format, - output_format); + "[placebo.convert] Converting from format %s to %s\n", + mlt_image_format_name(*format), + mlt_image_format_name(output_format)); if (placebo_frame_wants_tex(frame, output_format)) return upload_rgba(frame, image, format, width, height, gpu, rgba_fmt); @@ -161,20 +161,3 @@ static int convert_image(mlt_frame frame, return 1; } - -static mlt_frame filter_process(mlt_filter filter, mlt_frame frame) -{ - mlt_frame_push_convert_image(frame, convert_image); - return frame; -} - -mlt_filter filter_placebo_convert_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; - return filter; -} \ No newline at end of file diff --git a/src/modules/placebo/convert.h b/src/modules/placebo/convert.h new file mode 100644 index 000000000..8a97bd145 --- /dev/null +++ b/src/modules/placebo/convert.h @@ -0,0 +1,30 @@ +/* + * convert.h -- libplacebo image format converter + * Copyright (C) 2026 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 CONVERT_H +#define CONVERT_H + +#include + +int placebo_convert_image(mlt_frame frame, + uint8_t **image, + mlt_image_format *format, + mlt_image_format output_format); + +#endif // CONVERT_H diff --git a/src/modules/placebo/factory.c b/src/modules/placebo/factory.c index 47e665071..be8c8bde0 100644 --- a/src/modules/placebo/factory.c +++ b/src/modules/placebo/factory.c @@ -26,10 +26,6 @@ extern mlt_filter filter_placebo_render_init(mlt_profile profile, mlt_service_type type, const char *id, char *arg); -extern mlt_filter filter_placebo_convert_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, @@ -44,13 +40,8 @@ static mlt_properties metadata(mlt_service_type type, const char *id, void *data MLTPLACEBO_EXPORT MLT_REPOSITORY { - MLT_REGISTER(mlt_service_filter_type, "placebo.convert", filter_placebo_convert_init); 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.convert", - metadata, - "filter_placebo_convert.yml"); MLT_REGISTER_METADATA(mlt_service_filter_type, "placebo.render", metadata, diff --git a/src/modules/placebo/filter_placebo_convert.yml b/src/modules/placebo/filter_placebo_convert.yml deleted file mode 100644 index 5bda5c7cf..000000000 --- a/src/modules/placebo/filter_placebo_convert.yml +++ /dev/null @@ -1,14 +0,0 @@ -schema_version: 7.0 -type: filter -identifier: placebo.convert -title: GPU Format Converter (libplacebo) -version: 1 -copyright: Copyright (C) 2026 D-Ogi -creator: D-Ogi -license: LGPLv2.1 -language: en -tags: - - Video -description: > - Converts frame images between CPU pixel formats and the internal - libplacebo GPU texture format used by placebo.render and placebo.shader. \ No newline at end of file diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index e2bdca98c..d13317d87 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -17,6 +17,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "convert.h" #include "gpu_context.h" #include @@ -120,8 +121,9 @@ static int filter_get_image(mlt_frame frame, if (!placebo_frame_is_tex(frame, *format)) { mlt_log_error(MLT_FILTER_SERVICE(filter), - "Expected placebo private input, got %s\n", - mlt_image_format_name(*format)); + "Expected placebo private input, got %s (%s)\n", + mlt_image_format_name(*format), + mlt_properties_get(MLT_FRAME_PROPERTIES(frame), MLT_PLACEBO_IMAGE_PRIVATE)); return 1; } pl_tex src_tex = placebo_image_get_tex(*image); @@ -234,6 +236,7 @@ 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); + mlt_frame_prepend_convert_image(frame, placebo_convert_image); return frame; } diff --git a/src/modules/placebo/filter_placebo_shader.c b/src/modules/placebo/filter_placebo_shader.c index 6ab803abf..c6f24d6c2 100644 --- a/src/modules/placebo/filter_placebo_shader.c +++ b/src/modules/placebo/filter_placebo_shader.c @@ -17,6 +17,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ +#include "convert.h" #include "gpu_context.h" #include @@ -368,8 +369,9 @@ static int filter_get_image(mlt_frame frame, if (!placebo_frame_is_tex(frame, *format)) { mlt_log_error(MLT_FILTER_SERVICE(filter), - "Expected placebo private input, got %s\n", - mlt_image_format_name(*format)); + "Expected placebo private input, got %s (%s)\n", + mlt_image_format_name(*format), + mlt_properties_get(MLT_FRAME_PROPERTIES(frame), MLT_PLACEBO_IMAGE_PRIVATE)); return 1; } pl_tex src_tex = placebo_image_get_tex(*image); @@ -462,6 +464,7 @@ 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); + mlt_frame_prepend_convert_image(frame, placebo_convert_image); return frame; } diff --git a/src/modules/placebo/gpu_context.c b/src/modules/placebo/gpu_context.c index 79a7ef22f..0affcbe61 100644 --- a/src/modules/placebo/gpu_context.c +++ b/src/modules/placebo/gpu_context.c @@ -432,7 +432,8 @@ int placebo_frame_wants_tex(mlt_frame frame, mlt_image_format format) int placebo_frame_is_tex(mlt_frame frame, mlt_image_format format) { - const char *image_format = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), "mlt_image_private"); + const char *image_format = mlt_properties_get(MLT_FRAME_PROPERTIES(frame), + MLT_PLACEBO_IMAGE_PRIVATE); return format == mlt_image_private && image_format && !strcmp(image_format, "placebo"); } @@ -440,7 +441,7 @@ int placebo_frame_set_tex(mlt_frame frame, pl_tex tex) { mlt_properties props = MLT_FRAME_PROPERTIES(frame); mlt_properties_set_int(props, "format", mlt_image_private); - mlt_properties_set(props, "mlt_image_private", "placebo"); + mlt_properties_set(props, MLT_PLACEBO_IMAGE_PRIVATE, "placebo"); mlt_properties_clear(props, "alpha"); return mlt_frame_set_image(frame, (uint8_t *) tex, 0, placebo_tex_destroy); } diff --git a/src/modules/placebo/gpu_context.h b/src/modules/placebo/gpu_context.h index 4a14b2d36..5b5648913 100644 --- a/src/modules/placebo/gpu_context.h +++ b/src/modules/placebo/gpu_context.h @@ -27,6 +27,8 @@ #include +#define MLT_PLACEBO_IMAGE_PRIVATE "mlt_image_private" + /* Return the singleton pl_gpu, lazily initialized on first call. Returns NULL on failure. Thread-safe. */ pl_gpu placebo_gpu_get(void); From 2dcf6940e8f81ff69cde6c66e41df4d31dcde40b Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 14:41:24 -0700 Subject: [PATCH 22/24] fix cppcheck --- .github/workflows/static-code-analysis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/static-code-analysis.yml b/.github/workflows/static-code-analysis.yml index 57cc3622e..217fb7e7f 100644 --- a/.github/workflows/static-code-analysis.yml +++ b/.github/workflows/static-code-analysis.yml @@ -27,10 +27,7 @@ jobs: --library=cppcheck.cfg --suppress=ctuOneDefinitionRuleViolation --suppress=syntaxError:src/modules/xml/common.c - --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 + --suppress=syntaxError:src/modules/placebo/*.c steps: - uses: actions/checkout@v4 - name: Install Cppcheck From 705b3e0db6badc04bb1011b9cf247e451457a234 Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 16:52:57 -0700 Subject: [PATCH 23/24] update CMakeLists.txt to require libplacebo version 5.229 and simplify cppcheck suppression for placebo module --- CMakeLists.txt | 2 +- makefile | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e1582ab11..b31821d4d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -311,7 +311,7 @@ if(MOD_OPENFX) endif() if(MOD_PLACEBO) - pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo) + pkg_check_modules(libplacebo IMPORTED_TARGET libplacebo>=5.229) if(libplacebo_FOUND) list(APPEND MLT_SUPPORTED_COMPONENTS placebo) else() diff --git a/makefile b/makefile index 5abbec01a..309f4a2af 100644 --- a/makefile +++ b/makefile @@ -30,7 +30,4 @@ cppcheck: --library=cppcheck.cfg \ --suppress=ctuOneDefinitionRuleViolation \ --suppress=syntaxError:src/modules/xml/common.c \ - --suppress=syntaxError:src/modules/placebo/filter_placebo_convert.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 + --suppress=syntaxError:src/modules/placebo/*.c From 4c74b79fcbbd418977f65999bfd04e0a8131470d Mon Sep 17 00:00:00 2001 From: Dan Dennedy Date: Thu, 7 May 2026 17:30:22 -0700 Subject: [PATCH 24/24] improve placebo.render meta & cleanup --- src/modules/placebo/convert.c | 2 +- src/modules/placebo/filter_placebo_render.c | 3 +- src/modules/placebo/filter_placebo_render.yml | 39 +++++++++++++++++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/src/modules/placebo/convert.c b/src/modules/placebo/convert.c index 6bf844aea..2ea75fd37 100644 --- a/src/modules/placebo/convert.c +++ b/src/modules/placebo/convert.c @@ -150,7 +150,7 @@ int placebo_convert_image(mlt_frame frame, return 1; mlt_log_debug(NULL, - "[placebo.convert] Converting from format %s to %s\n", + "[placebo] Converting from format %s to %s\n", mlt_image_format_name(*format), mlt_image_format_name(output_format)); if (placebo_frame_wants_tex(frame, output_format)) diff --git a/src/modules/placebo/filter_placebo_render.c b/src/modules/placebo/filter_placebo_render.c index d13317d87..535a89869 100644 --- a/src/modules/placebo/filter_placebo_render.c +++ b/src/modules/placebo/filter_placebo_render.c @@ -107,8 +107,7 @@ static int filter_get_image(mlt_frame frame, return mlt_frame_get_image(frame, image, format, width, height, writable); } - /* Request placebo from upstream. placebo.convert is responsible for any - * CPU upload/download around non-placebo filters in the chain. */ + /* Request placebo private image format from upstream. */ placebo_frame_set_requested_tex(frame, 1); *format = mlt_image_private; int error = mlt_frame_get_image(frame, image, format, width, height, 1); diff --git a/src/modules/placebo/filter_placebo_render.yml b/src/modules/placebo/filter_placebo_render.yml index 6733ca06d..42708b90c 100644 --- a/src/modules/placebo/filter_placebo_render.yml +++ b/src/modules/placebo/filter_placebo_render.yml @@ -11,8 +11,8 @@ 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. + Uses D3D11 on Windows, Vulkan elsewhere. Useful as a finishing pass to reduce + banding artifacts and improve gradient quality before output. The compiled shader cache is stored in a platform-specific location and can be overridden by setting the MLT_PLACEBO_CACHE_PATH environment variable to an absolute path for the cache file. @@ -22,12 +22,16 @@ parameters: title: Quality Preset type: string description: > - Overall quality preset. "fast" minimizes GPU work, "default" is balanced, - "high_quality" enables all enhancements. + "fast" minimizes GPU work, "default" is balanced, "high_quality" enables + all enhancements. default: default mutable: yes readonly: no widget: combo + values: + - fast + - default + - high_quality - identifier: upscaler title: Upscaler @@ -39,6 +43,13 @@ parameters: mutable: yes readonly: no widget: combo + values: + - bilinear + - catmull_rom + - mitchell + - lanczos + - ewa_lanczos + - spline36 - identifier: downscaler title: Downscaler @@ -49,6 +60,13 @@ parameters: mutable: yes readonly: no widget: combo + values: + - bilinear + - catmull_rom + - mitchell + - lanczos + - ewa_lanczos + - spline36 - identifier: deband title: Debanding @@ -79,6 +97,11 @@ parameters: mutable: yes readonly: no widget: combo + values: + - blue + - ordered_lut + - white + - none - identifier: tonemapping title: Tone Mapping @@ -90,3 +113,11 @@ parameters: mutable: yes readonly: no widget: combo + values: + - auto + - clip + - mobius + - reinhard + - hable + - bt.2390 + - spline