Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,458 @@
# SPDX-FileCopyrightText: 2011-2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
.
algorithms
cached_resources
derived_resources
utilities
../nodes/geometry/include
../gpu/intern
../makesrna
../render/intern
../imbuf/opencolorio/intern/libocio
)
set(INC_SYS
)
set(SRC
COM_compile_state.hh
COM_context.hh
COM_conversion_operation.hh
COM_derived_resources.hh
COM_domain.hh
COM_group_input_node_operation.hh
COM_group_node_operation.hh
COM_group_output_node_operation.hh
COM_implicit_input_operation.hh
COM_input_descriptor.hh
COM_meta_data.hh
COM_multi_function_procedure_operation.hh
COM_node_group_operation.hh
COM_node_operation.hh
COM_operation.hh
COM_pixel_operation.hh
COM_realize_on_domain_operation.hh
COM_render_context.hh
COM_result.hh
COM_scheduler.hh
COM_shader_node.hh
COM_shader_operation.hh
COM_simple_operation.hh
COM_single_value_node_input_operation.hh
COM_static_cache_manager.hh
COM_undefined_node_operation.hh
COM_utilities.hh
intern/compile_state.cc
intern/context.cc
intern/conversion_operation.cc
intern/domain.cc
intern/group_input_node_operation.cc
intern/group_node_operation.cc
intern/group_output_node_operation.cc
intern/implicit_input_operation.cc
intern/meta_data.cc
intern/multi_function_procedure_operation.cc
intern/node_group_operation.cc
intern/node_operation.cc
intern/operation.cc
intern/pixel_operation.cc
intern/realize_on_domain_operation.cc
intern/render_context.cc
intern/result.cc
intern/scheduler.cc
intern/shader_node.cc
intern/shader_operation.cc
intern/simple_operation.cc
intern/single_value_node_input_operation.cc
intern/static_cache_manager.cc
intern/undefined_node_operation.cc
intern/utilities.cc
algorithms/intern/compute_preview.cc
algorithms/intern/convolve.cc
algorithms/intern/deriche_gaussian_blur.cc
algorithms/intern/extract_alpha.cc
algorithms/intern/jump_flooding.cc
algorithms/intern/morphological_blur.cc
algorithms/intern/morphological_distance.cc
algorithms/intern/morphological_distance_feather.cc
algorithms/intern/pad.cc
algorithms/intern/parallel_reduction.cc
algorithms/intern/recursive_gaussian_blur.cc
algorithms/intern/sample_pixel.cc
algorithms/intern/smaa.cc
algorithms/intern/summed_area_table.cc
algorithms/intern/symmetric_separable_blur.cc
algorithms/intern/symmetric_separable_blur_variable_size.cc
algorithms/intern/van_vliet_gaussian_blur.cc
algorithms/COM_algorithm_compute_preview.hh
algorithms/COM_algorithm_convolve.hh
algorithms/COM_algorithm_deriche_gaussian_blur.hh
algorithms/COM_algorithm_extract_alpha.hh
algorithms/COM_algorithm_jump_flooding.hh
algorithms/COM_algorithm_morphological_blur.hh
algorithms/COM_algorithm_morphological_distance.hh
algorithms/COM_algorithm_morphological_distance_feather.hh
algorithms/COM_algorithm_pad.hh
algorithms/COM_algorithm_parallel_reduction.hh
algorithms/COM_algorithm_recursive_gaussian_blur.hh
algorithms/COM_algorithm_sample_pixel.hh
algorithms/COM_algorithm_smaa.hh
algorithms/COM_algorithm_summed_area_table.hh
algorithms/COM_algorithm_symmetric_separable_blur.hh
algorithms/COM_algorithm_symmetric_separable_blur_variable_size.hh
algorithms/COM_algorithm_van_vliet_gaussian_blur.hh
cached_resources/intern/bokeh_kernel.cc
cached_resources/intern/cached_image.cc
cached_resources/intern/cached_mask.cc
cached_resources/intern/cached_shader.cc
cached_resources/intern/deriche_gaussian_coefficients.cc
cached_resources/intern/distortion_grid.cc
cached_resources/intern/fog_glow_kernel.cc
cached_resources/intern/image_coordinates.cc
cached_resources/intern/keying_screen.cc
cached_resources/intern/morphological_distance_feather_weights.cc
cached_resources/intern/ocio_color_space_conversion_shader.cc
cached_resources/intern/smaa_precomputed_textures.cc
cached_resources/intern/string_image.cc
cached_resources/intern/symmetric_blur_weights.cc
cached_resources/intern/symmetric_separable_blur_weights.cc
cached_resources/intern/van_vliet_gaussian_coefficients.cc
cached_resources/COM_bokeh_kernel.hh
cached_resources/COM_cached_image.hh
cached_resources/COM_cached_mask.hh
cached_resources/COM_cached_resource.hh
cached_resources/COM_cached_shader.hh
cached_resources/COM_deriche_gaussian_coefficients.hh
cached_resources/COM_distortion_grid.hh
cached_resources/COM_fog_glow_kernel.hh
cached_resources/COM_image_coordinates.hh
cached_resources/COM_keying_screen.hh
cached_resources/COM_morphological_distance_feather_weights.hh
cached_resources/COM_ocio_color_space_conversion_shader.hh
cached_resources/COM_smaa_precomputed_textures.hh
cached_resources/COM_string_image.hh
cached_resources/COM_symmetric_blur_weights.hh
cached_resources/COM_symmetric_separable_blur_weights.hh
cached_resources/COM_van_vliet_gaussian_coefficients.hh
derived_resources/intern/denoised_auxiliary_pass.cc
derived_resources/COM_denoised_auxiliary_pass.hh
utilities/intern/oidn.cc
utilities/COM_utilities_diagonals.hh
utilities/COM_utilities_oidn.hh
)
set(LIB
PRIVATE bf::blenfont
PRIVATE bf::blenkernel
PRIVATE bf::blentranslation
PRIVATE bf::functions
PRIVATE bf::gpu
PRIVATE bf::imbuf
PRIVATE bf::nodes
PRIVATE bf::render
PRIVATE bf::blenlib
PRIVATE bf::dna
PRIVATE bf::intern::guardedalloc
PRIVATE bf::intern::clog
PRIVATE bf::dependencies::opencolorio
PRIVATE bf::dependencies::optional::tbb
PRIVATE bf::dependencies::optional::fftw3
PRIVATE bf::dependencies::optional::openimagedenoise
)
set(GLSL_SRC
shaders/infos/compositor_alpha_crop_infos.hh
shaders/infos/compositor_bilateral_blur_infos.hh
shaders/infos/compositor_bokeh_blur_infos.hh
shaders/infos/compositor_bokeh_blur_variable_size_infos.hh
shaders/infos/compositor_bokeh_image_infos.hh
shaders/infos/compositor_box_mask_infos.hh
shaders/infos/compositor_compute_preview_infos.hh
shaders/infos/compositor_convert_infos.hh
shaders/infos/compositor_cryptomatte_infos.hh
shaders/infos/compositor_defocus_infos.hh
shaders/infos/compositor_deriche_gaussian_blur_infos.hh
shaders/infos/compositor_despeckle_infos.hh
shaders/infos/compositor_directional_blur_infos.hh
shaders/infos/compositor_displace_infos.hh
shaders/infos/compositor_double_edge_mask_infos.hh
shaders/infos/compositor_edge_filter_infos.hh
shaders/infos/compositor_ellipse_mask_infos.hh
shaders/infos/compositor_filter_infos.hh
shaders/infos/compositor_flip_infos.hh
shaders/infos/compositor_gamma_correct_infos.hh
shaders/infos/compositor_glare_infos.hh
shaders/infos/compositor_id_mask_infos.hh
shaders/infos/compositor_image_coordinates_infos.hh
shaders/infos/compositor_image_crop_infos.hh
shaders/infos/compositor_inpaint_infos.hh
shaders/infos/compositor_jump_flooding_infos.hh
shaders/infos/compositor_keying_infos.hh
shaders/infos/compositor_keying_screen_infos.hh
shaders/infos/compositor_kuwahara_infos.hh
shaders/infos/compositor_lens_distortion_infos.hh
shaders/infos/compositor_map_uv_infos.hh
shaders/infos/compositor_mask_to_sdf_infos.hh
shaders/infos/compositor_morphological_blur_infos.hh
shaders/infos/compositor_morphological_distance_feather_infos.hh
shaders/infos/compositor_morphological_distance_infos.hh
shaders/infos/compositor_morphological_distance_threshold_infos.hh
shaders/infos/compositor_morphological_step_infos.hh
shaders/infos/compositor_motion_blur_infos.hh
shaders/infos/compositor_movie_distortion_infos.hh
shaders/infos/compositor_node_tree_infos.hh
shaders/infos/compositor_normalize_infos.hh
shaders/infos/compositor_ocio_infos.hh
shaders/infos/compositor_pad_infos.hh
shaders/infos/compositor_parallel_reduction_infos.hh
shaders/infos/compositor_pixelate_infos.hh
shaders/infos/compositor_plane_deform_infos.hh
shaders/infos/compositor_premultiply_alpha_infos.hh
shaders/infos/compositor_realize_on_domain_infos.hh
shaders/infos/compositor_relative_to_pixel_infos.hh
shaders/infos/compositor_sample_pixel_infos.hh
shaders/infos/compositor_scale_variable_infos.hh
shaders/infos/compositor_smaa_infos.hh
shaders/infos/compositor_split_infos.hh
shaders/infos/compositor_summed_area_table_infos.hh
shaders/infos/compositor_symmetric_blur_infos.hh
shaders/infos/compositor_symmetric_blur_variable_size_infos.hh
shaders/infos/compositor_symmetric_separable_blur_infos.hh
shaders/infos/compositor_symmetric_separable_blur_variable_size_infos.hh
shaders/infos/compositor_tone_map_photoreceptor_infos.hh
shaders/infos/compositor_tone_map_simple_infos.hh
shaders/infos/compositor_translate_wrapped_infos.hh
shaders/infos/compositor_van_vliet_gaussian_blur_infos.hh
shaders/infos/compositor_write_output_infos.hh
shaders/infos/compositor_z_combine_infos.hh
shaders/compositor_alpha_crop.glsl
shaders/compositor_bilateral_blur.glsl
shaders/compositor_bokeh_blur.glsl
shaders/compositor_bokeh_blur_variable_size.glsl
shaders/compositor_bokeh_image.glsl
shaders/compositor_box_mask.glsl
shaders/compositor_compute_preview.glsl
shaders/compositor_convert.glsl
shaders/compositor_cryptomatte_image.glsl
shaders/compositor_cryptomatte_matte.glsl
shaders/compositor_cryptomatte_pick.glsl
shaders/compositor_defocus_blur.glsl
shaders/compositor_defocus_radius_from_depth.glsl
shaders/compositor_defocus_radius_from_scale.glsl
shaders/compositor_despeckle.glsl
shaders/compositor_deriche_gaussian_blur.glsl
shaders/compositor_deriche_gaussian_blur_sum.glsl
shaders/compositor_directional_blur.glsl
shaders/compositor_displace.glsl
shaders/compositor_displace_anisotropic.glsl
shaders/compositor_double_edge_mask_compute_boundary.glsl
shaders/compositor_double_edge_mask_compute_gradient.glsl
shaders/compositor_edge_filter.glsl
shaders/compositor_ellipse_mask.glsl
shaders/compositor_filter.glsl
shaders/compositor_flip.glsl
shaders/compositor_gamma_correct.glsl
shaders/compositor_glare_bloom_downsample.glsl
shaders/compositor_glare_bloom_upsample.glsl
shaders/compositor_glare_ghost_accumulate.glsl
shaders/compositor_glare_ghost_base.glsl
shaders/compositor_glare_highlights.glsl
shaders/compositor_glare_kernel_downsample.glsl
shaders/compositor_glare_mix.glsl
shaders/compositor_glare_simple_star_anti_diagonal_pass.glsl
shaders/compositor_glare_simple_star_diagonal_pass.glsl
shaders/compositor_glare_simple_star_horizontal_pass.glsl
shaders/compositor_glare_simple_star_vertical_pass.glsl
shaders/compositor_glare_streaks_accumulate.glsl
shaders/compositor_glare_streaks_filter.glsl
shaders/compositor_glare_sun_beams.glsl
shaders/compositor_glare_write_glare_output.glsl
shaders/compositor_glare_write_highlights_output.glsl
shaders/compositor_horizontal_lens_distortion.glsl
shaders/compositor_id_mask.glsl
shaders/compositor_image_coordinates_normalized.glsl
shaders/compositor_image_coordinates_pixel.glsl
shaders/compositor_image_coordinates_uniform.glsl
shaders/compositor_image_crop.glsl
shaders/compositor_inpaint_compute_boundary.glsl
shaders/compositor_inpaint_compute_region.glsl
shaders/compositor_inpaint_fill_region.glsl
shaders/compositor_jump_flooding.glsl
shaders/compositor_keying_compute_image.glsl
shaders/compositor_keying_compute_matte.glsl
shaders/compositor_keying_extract_chroma.glsl
shaders/compositor_keying_replace_chroma.glsl
shaders/compositor_keying_screen.glsl
shaders/compositor_keying_tweak_matte.glsl
shaders/compositor_kuwahara_anisotropic.glsl
shaders/compositor_kuwahara_anisotropic_compute_structure_tensor.glsl
shaders/compositor_kuwahara_classic.glsl
shaders/compositor_map_uv.glsl
shaders/compositor_map_uv_anisotropic.glsl
shaders/compositor_mask_to_sdf_compute_boundary.glsl
shaders/compositor_mask_to_sdf_compute_distance.glsl
shaders/compositor_morphological_blur.glsl
shaders/compositor_morphological_distance.glsl
shaders/compositor_morphological_distance_feather.glsl
shaders/compositor_morphological_distance_threshold.glsl
shaders/compositor_morphological_distance_threshold_seeds.glsl
shaders/compositor_morphological_step.glsl
shaders/compositor_motion_blur.glsl
shaders/compositor_motion_blur_max_velocity_dilate.glsl
shaders/compositor_movie_distortion.glsl
shaders/compositor_normalize.glsl
shaders/compositor_pad.glsl
shaders/compositor_parallel_reduction.glsl
shaders/compositor_pixelate.glsl
shaders/compositor_plane_deform_anisotropic.glsl
shaders/compositor_plane_deform_mask.glsl
shaders/compositor_plane_deform_motion_blur.glsl
shaders/compositor_plane_deform_motion_blur_mask.glsl
shaders/compositor_plane_deform.glsl
shaders/compositor_premultiply_alpha.glsl
shaders/compositor_radial_lens_distortion.glsl
shaders/compositor_realize_on_domain.glsl
shaders/compositor_relative_to_pixel_float.glsl
shaders/compositor_relative_to_pixel_float_per_dimension.glsl
shaders/compositor_relative_to_pixel_vector.glsl
shaders/compositor_sample_pixel.glsl
shaders/compositor_scale_variable.glsl
shaders/compositor_smaa_blending_weight_calculation.glsl
shaders/compositor_smaa_edge_detection.glsl
shaders/compositor_smaa_neighborhood_blending.glsl
shaders/compositor_split.glsl
shaders/compositor_summed_area_table_compute_complete_blocks.glsl
shaders/compositor_summed_area_table_compute_complete_x_prologues.glsl
shaders/compositor_summed_area_table_compute_complete_y_prologues.glsl
shaders/compositor_summed_area_table_compute_incomplete_prologues.glsl
shaders/compositor_symmetric_blur.glsl
shaders/compositor_symmetric_blur_variable_size.glsl
shaders/compositor_symmetric_separable_blur.glsl
shaders/compositor_symmetric_separable_blur_variable_size.glsl
shaders/compositor_tone_map_photoreceptor.glsl
shaders/compositor_tone_map_simple.glsl
shaders/compositor_translate_wrapped.glsl
shaders/compositor_van_vliet_gaussian_blur.glsl
shaders/compositor_van_vliet_gaussian_blur_sum.glsl
shaders/compositor_write_output.glsl
shaders/compositor_z_combine_compute_mask.glsl
shaders/compositor_z_combine_from_mask_depth.glsl
shaders/compositor_z_combine_from_mask_image.glsl
shaders/compositor_z_combine_simple_depth.glsl
shaders/compositor_z_combine_simple_image.glsl
shaders/library/gpu_shader_compositor_alpha_over.glsl
shaders/library/gpu_shader_compositor_bright_contrast.glsl
shaders/library/gpu_shader_compositor_channel_matte.glsl
shaders/library/gpu_shader_compositor_chroma_matte.glsl
shaders/library/gpu_shader_compositor_code_generation.glsl
shaders/library/gpu_shader_compositor_color_balance.glsl
shaders/library/gpu_shader_compositor_color_correction.glsl
shaders/library/gpu_shader_compositor_color_matte.glsl
shaders/library/gpu_shader_compositor_color_spill.glsl
shaders/library/gpu_shader_compositor_color_to_luminance.glsl
shaders/library/gpu_shader_compositor_convert_alpha.glsl
shaders/library/gpu_shader_compositor_difference_matte.glsl
shaders/library/gpu_shader_compositor_distance_matte.glsl
shaders/library/gpu_shader_compositor_eval.glsl
shaders/library/gpu_shader_compositor_exposure.glsl
shaders/library/gpu_shader_compositor_hue_correct.glsl
shaders/library/gpu_shader_compositor_hue_saturation_value.glsl
shaders/library/gpu_shader_compositor_image_diagonals.glsl
shaders/library/gpu_shader_compositor_invert.glsl
shaders/library/gpu_shader_compositor_jump_flooding_lib.glsl
shaders/library/gpu_shader_compositor_luminance_matte.glsl
shaders/library/gpu_shader_compositor_main.glsl
shaders/library/gpu_shader_compositor_map_value.glsl
shaders/library/gpu_shader_compositor_motion_blur_lib.glsl
shaders/library/gpu_shader_compositor_nodetree_type.glsl
shaders/library/gpu_shader_compositor_ocio_processor.glsl
shaders/library/gpu_shader_compositor_ocio_processor_lib.glsl
shaders/library/gpu_shader_compositor_posterize.glsl
shaders/library/gpu_shader_compositor_separate_combine.glsl
shaders/library/gpu_shader_compositor_set_alpha.glsl
shaders/library/gpu_shader_compositor_set_value.glsl
shaders/library/gpu_shader_compositor_store.glsl
shaders/library/gpu_shader_compositor_store_output.glsl
shaders/library/gpu_shader_compositor_summed_area_table_lib.glsl
shaders/library/gpu_shader_compositor_texture_utilities.glsl
shaders/library/gpu_shader_compositor_type_conversion.glsl
shaders/library/gpu_shader_function_combine_matrix.glsl
shaders/library/gpu_shader_function_invert_matrix.glsl
shaders/library/gpu_shader_function_matrix_determinant.glsl
shaders/library/gpu_shader_function_matrix_multiply.glsl
shaders/library/gpu_shader_function_project_point.glsl
shaders/library/gpu_shader_function_separate_matrix.glsl
shaders/library/gpu_shader_function_transform_direction.glsl
shaders/library/gpu_shader_function_transform_point.glsl
shaders/library/gpu_shader_function_transpose_matrix.glsl
)
set(GLSL_INC
../gpu
../gpu/intern
../gpu/shaders
../gpu/shaders/common
shaders
shaders/infos
shaders/library
)
set(GLSL_C "")
foreach(GLSL_FILE ${GLSL_SRC})
glsl_to_c(${GLSL_FILE} GLSL_C GLSL_INC)
endforeach()
blender_add_lib(bf_compositor_shaders "${GLSL_C}" "" "" "")
blender_set_target_unity_build(bf_compositor_shaders 10)
list(APPEND LIB
bf_compositor_shaders
)
set(GLSL_SOURCE_CONTENT "")
set(GLSL_METADATA_CONTENT "")
set(GLSL_INFOS_CONTENT "")
foreach(GLSL_FILE ${GLSL_SRC})
get_filename_component(GLSL_FILE_NAME ${GLSL_FILE} NAME)
string(REPLACE "." "_" GLSL_FILE_NAME_UNDERSCORES ${GLSL_FILE_NAME})
string(APPEND GLSL_FILE_NAME_UNDERSCORES "_tmp")
string(APPEND GLSL_SOURCE_CONTENT "SHADER_SOURCE\(${GLSL_FILE_NAME_UNDERSCORES}, \"${GLSL_FILE_NAME}\", \"${GLSL_FILE}\"\)\n")
string(APPEND GLSL_METADATA_CONTENT "#include \"${GLSL_FILE}.hh\"\n")
string(APPEND GLSL_INFOS_CONTENT "#include \"${GLSL_FILE}.info\"\n")
endforeach()
set(glsl_source_list_file "${CMAKE_CURRENT_BINARY_DIR}/glsl_compositor_source_list.h")
file(GENERATE OUTPUT ${glsl_source_list_file} CONTENT "${GLSL_SOURCE_CONTENT}")
list(APPEND SRC ${glsl_source_list_file})
set(glsl_metadata_list_file "${CMAKE_CURRENT_BINARY_DIR}/glsl_compositor_metadata_list.hh")
file(GENERATE OUTPUT ${glsl_metadata_list_file} CONTENT "${GLSL_METADATA_CONTENT}")
list(APPEND SRC ${glsl_metadata_list_file})
set(glsl_infos_list_file "${CMAKE_CURRENT_BINARY_DIR}/glsl_compositor_infos_list.hh")
file(GENERATE OUTPUT ${glsl_infos_list_file} CONTENT "${GLSL_INFOS_CONTENT}")
list(APPEND SRC ${glsl_infos_list_file})
list(APPEND INC ${CMAKE_CURRENT_BINARY_DIR})
target_include_directories(bf_compositor_shaders PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
blender_add_lib(bf_compositor "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
if(CXX_WARN_NO_SUGGEST_OVERRIDE)
target_compile_options(bf_compositor PRIVATE $<$<COMPILE_LANGUAGE:CXX>:-Wsuggest-override>)
endif()

View File

@@ -0,0 +1,204 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_vector_set.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_node_operation.hh"
#include "COM_pixel_operation.hh"
namespace blender::compositor {
struct Schedule;
/* ------------------------------------------------------------------------------------------------
* Compile State
*
* The compile state is a utility class used to track the state of compilation when compiling the
* node tree. In particular, it tracks two important pieces of information, each of which is
* described in one of the following sections.
*
* First, it stores a mapping between all nodes and the operations they were compiled into. The
* mapping are stored independently depending on the type of the operation in the node_operations_
* and pixel_operations_ maps. So those two maps are mutually exclusive. The compiler should call
* the map_node_to_node_operation and map_node_to_pixel_operation methods to populate those maps
* as soon as it compiles a node or multiple nodes into an operation. Those maps are used to
* retrieve the results of outputs linked to the inputs of operations. For more details, see the
* get_result_from_output_socket method. For the node tree shown below, nodes 1, 2, and 6 are
* mapped to their compiled operations in the node_operation_ map. While nodes 3 and 4 are both
* mapped to the first pixel operation, and node 5 is mapped to the second pixel operation in the
* pixel_operations_ map.
*
* Pixel Operation 1 Pixel Operation 2
* +-----------------------------------+ +------------------+
* .------------. | .------------. .------------. | | .------------. | .------------.
* | Node 1 | | | Node 3 | | Node 4 | | | | Node 5 | | | Node 6 |
* | |----|--| |--| |---|-----|--| |--|--| |
* | | .-|--| | | | | .--|--| | | | |
* '------------' | | '------------' '------------' | | | '------------' | '------------'
* | +-----------------------------------+ | +------------------+
* .------------. | |
* | Node 2 | | |
* | |--'----------------------------------------'
* | |
* '------------'
*
* Second, it stores the pixel compile unit, whether is operates on single values, and its domain
* if it was not operating on single values. One should first go over the discussion in
* COM_evaluator.hh for a high level description of the mechanism of the compile unit. The one
* important detail in this class is the should_compile_pixel_compile_unit method, which implements
* the criteria of whether the compile unit should be compiled given the node currently being
* processed as an argument. Those criteria are described as follows. If the compile unit is empty
* as is the case when processing nodes 1, 2, and 3, then it plainly shouldn't be compiled. If the
* given node is not a pixel node, then it can't be added to the compile unit and the unit is
* considered complete and should be compiled, as is the case when processing node 6. If the
* compile unit operates on single values and the given node operates on non-single values or vice
* versa, then it can't be added to the compile unit and the unit is considered complete and should
* be compiled, more on that in the next section. If the computed domain of the given node is not
* compatible with the domain of the compiled unit, then it can't be added to the unit and the unit
* is considered complete and should be compiled, as is the case when processing node 5, more on
* this in the next section. Otherwise, the given node is compatible with the compile unit and can
* be added to it, so the unit shouldn't be compiled just yet, as is the case when processing
* node 4.
*
* Special attention should be given to the aforementioned single value and domain compatibility
* criterion. One should first go over the discussion in COM_domain.hh for more information on
* domains. When a compile unit gets eventually compiled to a pixel operation, that operation will
* have a certain operation domain, and any node that gets added to the compile unit should itself
* have a computed node domain that is compatible with that operation domain, otherwise, had the
* node been compiled into its own operation separately, the result would have been be different.
* For instance, consider the above node tree where node 1 outputs a 100x100 result, node 2 outputs
* a 50x50 result, the first input in node 3 has the highest domain priority, and the second input
* in node 5 has the highest domain priority. In this case, pixel operation 1 will output a 100x100
* result, and pixel operation 2 will output a 50x50 result, because that's the computed operation
* domain for each of them. So node 6 will get a 50x50 result. Now consider the same node tree, but
* where all three nodes 3, 4, and 5 were compiled into a single pixel operation as shown the node
* tree below. In that case, pixel operation 1 will output a 100x100 result, because that's its
* computed operation domain. So node 6 will get a 100x100 result. As can be seen, the final result
* is different even though the node tree is the same. That's why the compiler can decide to
* compile the compile unit early even though further nodes can still be technically added to it.
*
* Pixel Operation 1
* +------------------------------------------------------+
* .------------. | .------------. .------------. .------------. | .------------.
* | Node 1 | | | Node 3 | | Node 4 | | Node 5 | | | Node 6 |
* | |----|--| |--| |------| |--|--| |
* | | .-|--| | | | .---| | | | |
* '------------' | | '------------' '------------' | '------------' | '------------'
* | +----------------------------------|-------------------+
* .------------. | |
* | Node 2 | | |
* | |--'------------------------------------'
* | |
* '------------'
*
* Similarly, all nodes in the compile unit should either be operating on single values or not.
* Otherwise, assuming a node operates on single values and its output is used in 1) a non-single
* value pixel operation and 2) another node that expects single values, if that node was added to
* the pixel operation, its output will be non-single value, while it would have been a single
* value if it was not added to the pixel operation.
*
* To check for the single value type and domain compatibility between the compile unit and the
* node being processed, the single value type and the domain of the compile unit is assumed to be
* the single value type and the domain of the first node added to the compile unit, noting that
* the domain is optional, since it is not used if the compile unit is a single value one. The
* single value type and the domain of the compile unit are computed and set in the
* add_node_to_pixel_compile_unit method. When processing a node, the computed single value type
* and the computed domain of node are compared to the compile unit single value type and domain in
* the should_compile_pixel_compile_unit method. Node single value types and domains are computed
* in the is_pixel_node_single_value and compute_pixel_node_domain methods respectively, the latter
* of which is analogous to the Operation::compute_domain method for nodes that are not yet
* compiled. */
class CompileState {
private:
/* A reference to the compositor context. */
const Context &context_;
/* A reference to the node execution schedule that is being compiled. */
const Schedule &schedule_;
/* Those two maps associate each node with the operation it was compiled into. Each node is
* either compiled into a node operation and added to node_operations, or compiled into a pixel
* operation and added to pixel_operations. Those maps are used to retrieve the results of
* outputs linked to the inputs of operations. See the get_result_from_output_socket method for
* more information. */
Map<const bNode *, NodeOperation *> node_operations_;
Map<const bNode *, PixelOperation *> pixel_operations_;
/* A contiguous subset of the node execution schedule that contains the group of nodes that will
* be compiled together into a pixel operation. See the discussion in COM_evaluator.hh for more
* information. */
PixelCompileUnit pixel_compile_unit_;
/* Stores whether the current pixel compile unit operates on single values. Only initialized when
* the pixel compile unit is not empty. */
bool is_pixel_compile_unit_single_value_;
/* The domain of the pixel compile unit if it was not a single value. Only initialized when the
* pixel compile unit is not empty and is not a single value. */
std::optional<Domain> pixel_compile_unit_domain_;
public:
/* Construct a compile state from the node group execution schedule being compiled. */
CompileState(const Context &context, const Schedule &schedule);
/* Get a reference to the node execution schedule being compiled. */
const Schedule &get_schedule();
/* Add an association between the given node and the give node operation that the node was
* compiled into in the node_operations_ map. */
void map_node_to_node_operation(const bNode &node, NodeOperation *operation);
/* Add an association between the given node and the give pixel operation that the node was
* compiled into in the pixel_operations_ map. */
void map_node_to_pixel_operation(const bNode &node, PixelOperation *operation);
/* Returns a reference to the result of the operation corresponding to the given output that the
* given output's node was compiled to. */
Result &get_result_from_output_socket(const bNodeSocket &output);
/* Add the given node to the compile unit. And if the domain of the compile unit is not yet
* determined or was determined to be an identity domain, update it to the computed domain for
* the give node. */
void add_node_to_pixel_compile_unit(const bNode &node);
/* Get a reference to the pixel compile unit. */
PixelCompileUnit &get_pixel_compile_unit();
/* Returns true if the pixel compile unit operates on single values. */
bool is_pixel_compile_unit_single_value();
/* Clear the compile unit. This should be called once the compile unit is compiled to ready it to
* track the next potential compile unit. */
void reset_pixel_compile_unit();
/* Determines if the compile unit should be compiled based on a number of criteria give the node
* currently being processed. See the class description for a description of the method. */
bool should_compile_pixel_compile_unit(const bNode &node);
/* Identify of the number of outputs of the pixel compile unit surpass what is possible. This is
* essentially the number of outputs that will be added for the nodes in the pixel compile unit
* in ShaderOperation::populate_results_for_node. */
bool pixel_compile_unit_has_too_many_outputs(const bool are_node_previews_needed);
/* Identify of the number of inputs of the pixel compile unit surpass what is possible. This is
* essentially the number of inputs that will be added for the nodes in the pixel compile unit in
* ShaderOperation::link_node_inputs. */
bool pixel_compile_unit_has_too_many_inputs();
private:
/* Determines if the given pixel node operates on single values or not. The node operates on
* single values if all its inputs are single values, and consequently will also output single
* values. */
bool is_pixel_node_single_value(const bNode &node);
/* Compute the node domain of the given pixel node. This is analogous to the
* Operation::compute_domain method, except it is computed from the node itself as opposed to a
* compiled operation. See the discussion in COM_domain.hh for more information. */
Domain compute_pixel_node_domain(const bNode &node);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,144 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_bounds_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "GPU_shader.hh"
#include "BKE_compute_context_cache.hh"
#include "COM_domain.hh"
#include "COM_meta_data.hh"
#include "COM_render_context.hh"
#include "COM_result.hh"
#include "COM_static_cache_manager.hh"
namespace blender {
struct Main;
} // namespace blender
namespace blender::nodes::eval_log {
class NodesEvalLog;
} // namespace blender::nodes::eval_log
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Context
*
* A Context is an abstract class that is implemented by the caller of the evaluator to provide the
* necessary data and functionalities for the correct operation of the evaluator. This includes
* providing input data like render passes and the active scene, as well as callbacks to write the
* outputs of the compositor. Finally, the class have a reference to a static resource manager for
* acquiring cached resources efficiently. */
class Context {
private:
/* A static cache manager that can be used to acquire cached resources for the compositor
* efficiently. */
StaticCacheManager &cache_manager_;
public:
Context(StaticCacheManager &cache_manager);
virtual const Main &get_main() const = 0;
/* Get the compositing scene. */
virtual const Scene &get_scene() const = 0;
/* Returns the domain that the inputs and outputs of the context will be in. */
virtual Domain get_compositing_domain() const = 0;
/* Write the result of the compositor viewer. */
virtual void write_viewer(Result &viewer_result) = 0;
/* True if the compositor should use GPU acceleration. */
virtual bool use_gpu() const = 0;
/* Get the strip that the compositing modifier is applied to. */
virtual const Strip *get_strip() const;
/* Get the pass with the given name in the given view layer and scene. Freeing the pass is the
* caller's responsibility. */
virtual Result get_pass(const Scene *scene, int view_layer, const char *name);
/* Get the render settings for compositing. This could be different from scene->r render settings
* in case the render size or other settings needs to be overwritten. */
virtual const RenderData &get_render_data() const;
/* Get the name of the view currently being rendered. If the context is not multi-view, return an
* empty string. */
virtual StringRef get_view_name() const;
/* Get the precision of the intermediate results of the compositor. */
virtual ResultPrecision get_precision() const;
/* Set an info message. This is called by the compositor evaluator to inform or warn the user
* about something, typically an error. The implementation should display the message in an
* appropriate place, which can be directly in the UI or just logged to the output stream. */
virtual void set_info_message(StringRef message) const;
/* True if the compositor should treat viewer nodes as group output nodes because it has no
* concept of or support for viewers. */
virtual bool treat_viewer_as_group_output() const;
/* Populates the given meta data from the render stamp information of the given render pass. */
virtual void populate_meta_data_for_pass(const Scene *scene,
int view_layer_id,
const char *pass_name,
MetaData &meta_data) const;
/* Get a pointer to the render context of this context. A render context stores information about
* the current render. It might be null if the compositor is not being evaluated as part of a
* render pipeline. */
virtual RenderContext *render_context() const;
/* Returns a pointer to a nodes evaluation log of the context, this can be nullptr for context
* that does not support logging. */
virtual nodes::eval_log::NodesEvalLog *nodes_evaluation_log() const;
/* Gets called after the evaluation of each compositor operation. See overrides for possible
* uses. */
virtual void evaluate_operation_post() const;
/* Returns true if the compositor evaluation is canceled and that the evaluator should stop
* executing as soon as possible. */
virtual bool is_canceled() const;
/* Get the normalized render percentage of the active scene. */
float get_render_percentage() const;
/* Get the current frame number of the active scene. */
int get_frame_number() const;
/* Get the current time in seconds of the active scene. */
float get_time() const;
/* Get the OIDN denoiser quality which should be used if the user doesn't explicitly set
* denoising quality on a node. */
eCompositorDenoiseQaulity get_denoise_quality() const;
/* Get a GPU shader with the given info name and precision. */
gpu::Shader *get_shader(const char *info_name, ResultPrecision precision);
/* Get a GPU shader with the given info name and context's precision. */
gpu::Shader *get_shader(const char *info_name);
/* Create a result of the given type and precision. */
Result create_result(ResultType type, ResultPrecision precision);
/* Create a result of the given type using the context's precision. */
Result create_result(ResultType type);
/* Get a reference to the static cache manager of this context. */
StaticCacheManager &cache_manager();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
#include "COM_simple_operation.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Conversion Operation
*
* A simple operation that converts a result from a certain type to another. See the derived
* classes for more details. */
class ConversionOperation : public SimpleOperation {
public:
ConversionOperation(Context &context,
const ResultType input_type,
const ResultType expected_type);
/* If the input result is a single value, execute_single is called. Otherwise, the shader
* provided by get_conversion_shader is dispatched for GPU contexts or execute_cpu is called for
* CPU contexts. */
void execute() override;
/* Determine if a conversion operation is needed for the input with the given result and
* descriptor. If it is not needed, return a null pointer. If it is needed, return an instance of
* the appropriate conversion operation. */
static SimpleOperation *construct_if_needed(Context &context,
const Result &input_result,
const InputDescriptor &input_descriptor);
private:
/* Convert the input single value result to the output single value result. */
void execute_single(const Result &input, Result &output);
/* Convert the input to the appropriate type and write the result to the output on the CPU. */
void execute_cpu(const Result &input, Result &output);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,36 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_denoised_auxiliary_pass.hh"
namespace blender::compositor {
/* -------------------------------------------------------------------------------------------------
* Derived Resources.
*
* Derived resources are resources that are computed from a particular result, stored in it, and
* freed when the result is freed. The same resources might be needed by multiple operations, so
* caching them on the result will improve performance at the cost of higher memory usage.
*
* The DerivedResources class stores instances of the container classes that store derived
* resources. This is very similar in design to the StaticCacheManager, see its description for
* more information. Destroying an instance of this class is expected to destroy all derived
* resources in it.
*
* To add a new derived resource:
*
* - Create a key class that can be used to identify the resource in a Map if needed.
* - Create a resource class to compute and store the resource.
* - Create a container class to store the resources in a map identified by their keys.
* - Add an instance of the container to the DerivedResources class.
*
* See the existing derived resources for reference. */
class DerivedResources {
public:
DenoisedAuxiliaryPassContainer denoised_auxiliary_passes;
};
} // namespace blender::compositor

View File

@@ -0,0 +1,212 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "GPU_texture.hh"
namespace blender::compositor {
/* Possible interpolations to use when realizing an input result of some domain on another domain.
* See the RealizationOptions struct for more information. */
enum class Interpolation : uint8_t {
Nearest,
Bilinear,
Bicubic,
Anisotropic,
};
/* Possible extensions when computing samples in the domain's exterior. */
enum class Extension : uint8_t {
/* Areas outside of the image are filled with the closest boundary pixel in the image. */
Extend,
/* Areas outside of the image are filled with repetitions of the image. */
Repeat,
/* Areas outside of the image are filled with zero. */
Clip,
};
/* ------------------------------------------------------------------------------------------------
* Realization Options
*
* The options that describe how an input result prefer to be realized on some other domain. This
* is used by the Realize On Domain and Transform algorithms to identify the appropriate method of
* realization. See the Domain class for more information. */
struct RealizationOptions {
/* The interpolation method that should be used when performing realization. Since realizing a
* result involves projecting it on a different domain, which in turn, involves sampling the
* result at arbitrary locations, the interpolation identifies the method used for computing the
* value at those arbitrary locations. */
Interpolation interpolation = Interpolation::Bilinear;
/* The extend mode for the x-axis. Defaults to Zero padding. */
Extension extension_x = Extension::Clip;
/* The extend mode for the y-axis. Defaults to Zero padding. */
Extension extension_y = Extension::Clip;
};
/* ------------------------------------------------------------------------------------------------
* Domain
*
* The compositor is designed in such a way as to allow compositing in an infinite virtual
* compositing space. Consequently, any result of an operation is not only represented by its image
* pixel data, but also by its transformation in that virtual space, both of which are stored and
* represented by a Domain. In the figure below, two results of different domains are illustrated
* on the virtual compositing space. One of the results is centered in space with an image
* dimension of 800px x 600px, and the other result is scaled down and translated such that it lies
* in the upper right quadrant of the space with an image dimension of 800px × 400px. The position
* of the domain is in pixel space, and the domain is considered centered if it has an identity
* transformation. Note that both results have the same resolution, but occupy different areas of
* the virtual compositing space.
*
* y
* ^
* 800px x 600px |
* .---------------------|---------------------.
* | | 800px x 600px |
* | | .-------------. |
* | | | | |
* | | '-------------' |
* ------|---------------------|---------------------|------> x
* | | |
* | | |
* | | |
* | | |
* '---------------------|---------------------'
* |
*
* Additionally, the image might not have pixel data defined everywhere or might have data that
* extends outside of boundary of the image, the latter is typically known as over-scan. Therefore,
* the domain stores the image information as:
*
* - data_size: Which is the size of the data that actually exists in pixels.
* - display_size: Which is the conceptual and visual size of the image in pixels.
* - data_offset: Which is the offset from the lower left corner of the display region to the data
* region in pixels. See the member documentation for more information.
*
* See the EXR Data/Display Window specification for more information on this formulation. The only
* deviation from EXR is that we store the offset of the display window as the translation of the
* transformation, which works better in practice.
*
* By default, results have domains of identity transformations, that is, they are centered in
* space, but a transformation operation like the rotate, translate, or transform operations will
* adjust the transformation to make the result reside somewhere different in space. The domain of
* a single value result is irrelevant and always set to an identity domain.
*
* An operation is typically only concerned about a subset of the virtual compositing space, this
* subset is represented by a domain which is called the Operation Domain. It follows that before
* the operation itself is executed, inputs will typically be realized on the operation domain to
* be in the same domain and have the same dimension as that of the operation domain. This process
* is called Domain Realization and is implemented using an operation called the Realize On Domain
* Operation. Realization involves projecting the result onto the target domain, copying the area
* of the result that intersects the target domain, and filling the rest with zeros or repetitions
* of the result depending on the realization options that can be set by the user. Consequently,
* operations can generally expect their inputs to have the same dimension and can operate on them
* directly and transparently. For instance, if an operation takes both results illustrated in
* the figure above, and the operation has an operation domain that matches the bigger domain, the
* result with the bigger domain will not need to be realized because it already has a domain that
* matches that of the operation domain, but the result with the smaller domain will have to be
* realized into a new result that has the same domain as the domain of the bigger result. Assuming
* no repetition, the output of the realization will be an all zeros image with dimension 800px ×
* 600px with a small scaled version of the smaller result copied into the upper right quadrant of
* the image. The following figure illustrates the realization process on a different set of
* results
*
* Realized Result
* +-------------+ +-------------+
* | Operation | | |
* | Domain | | Zeros |
* | | ----> | |
* +-----|-----+ | |-----+ |
* | | C | | | C | |
* | +-----|-------+ +-----'-------+
* | Domain Of |
* | Input |
* +-----------+
*
* An operation can operate in an arbitrary operation domain, but in most cases, the operation
* domain is inferred from the inputs of the operation. In particular, one of the inputs is said to
* be the Domain Input of the operation and the operation domain is inferred from its domain. It
* follows that this particular input will not need realization, because it already has the correct
* domain. The domain input selection mechanism is as follows. Each of the inputs are assigned a
* value by the developer called the Domain Priority, the domain input is then chosen as the
* non-single value input with the highest domain priority, zero being the highest priority. See
* Operation::compute_domain for more information.
*
* The aforementioned logic for operation domain computation is only a default that works for most
* cases, but an operation can override the compute_domain method to implement a different logic.
* For instance, output nodes have an operation domain the same size as the viewport and with an
* identity transformation, their operation domain doesn't depend on the inputs at all.
*
* For instance, a filter operation has two inputs, a factor and a color, the latter of which is
* assigned a domain priority of 0 and the former is assigned a domain priority of 1. If the color
* input is not a single value input, then the color input is considered to be the domain input of
* the operation and the operation domain is computed to be the same domain as the color input,
* because it has the highest priority. It follows that if the factor input has a different domain
* than the computed domain of the operation, it will be projected and realized on it to have the
* same domain as described above. On the other hand, if the color input is a single value input,
* then the factor input is considered to be the domain input and the operation domain will be the
* same as the domain of the factor input, because it has the second highest domain priority.
* Finally, if both inputs are single value inputs, the operation domain will be an identity domain
* and is irrelevant, because the output will be a domain-less single value. */
class Domain {
public:
/* The size of the data that actually exists in pixels. */
int2 data_size;
/* The conceptual/visual size or of the image in pixels. */
int2 display_size;
/* The offset from the display region to the data region. A positive data offset means the data
* only covers a portion of the display, while a negative data offset means that the data extends
* past the boundary of the display, that is, over-scan exists. */
int2 data_offset;
/* The 2D transformation of the domain defining its translation in pixels, rotation, and scale in
* the virtual compositing space. */
float3x3 transformation;
/* The options that describe how this domain prefer to be realized on some other domain. See the
* RealizationOptions struct for more information. */
RealizationOptions realization_options;
/* A size only constructor that sets the transformation to identity. */
Domain(const int2 &size);
Domain(const int2 &size, const float3x3 &transformation);
/* Transform the domain by the given transformation. This effectively pre-multiply the given
* transformation by the current transformation of the domain. */
void transform(const float3x3 &input_transformation);
/* Returns a transposed version of itself, that is, with the x and y sizes swapped. */
Domain transposed() const;
/* Compute a domain from this potential transformed domain such that its rotation and scale
* become identity and the size of the domain is increased/reduced to adapt to the new
* transformation. For instance, if the domain is rotated, the returned domain will have zero
* rotation but expanded size to account for the bounding box of the domain after rotation.
* Similarly, if the realize_translation argument is true, translation will be set to zero,
* though this will not affect the size of the domain in any way. */
Domain realize_transformation(const bool realize_translation = false) const;
/* Returns a domain of size 1x1 and an identity transformation. */
static Domain identity();
/* Compare the size and transformation of the domain. Transformations are compared within the
* given epsilon. The realization_options are not compared because they only describe the method
* of realization on another domain, which is not technically a property of the domain itself. */
static bool is_equal(const Domain &a, const Domain &b, const float epsilon = 10e-6f);
};
/* Identical to the is_equal static method with zero epsilon. */
bool operator==(const Domain &a, const Domain &b);
bool operator!=(const Domain &a, const Domain &b);
StringRefNull to_string(const Interpolation &interpolation);
StringRefNull to_string(const Extension &extension);
GPUSamplerExtendMode map_extension_mode_to_extend_mode(const Extension &mode);
} // namespace blender::compositor

View File

@@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_node_types.h"
#include "COM_context.hh"
#include "COM_node_group_operation.hh"
namespace blender::compositor {
/* Returns an instance of a new GroupInputNodeOperation with the given parameters. See the class
* for more information. */
NodeOperation *get_group_input_node_operation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation);
} // namespace blender::compositor

View File

@@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_node_group_operation.hh"
#include "COM_node_operation.hh"
namespace blender::compositor {
/* Returns an instance of a new GroupNodeOperation with the given parameters. See the class for
* more information.. */
NodeOperation *get_group_node_operation(Context &context,
const bNode &node,
const NodeGroupOutputTypes &needed_outputs,
const bNodeInstanceKey active_node_group_instance_key);
} // namespace blender::compositor

View File

@@ -0,0 +1,20 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_node_types.h"
#include "COM_context.hh"
#include "COM_node_group_operation.hh"
namespace blender::compositor {
/* Returns an instance of a new GroupOutputNodeOperation with the given parameters. See the class
* for more information. */
NodeOperation *get_group_output_node_operation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation);
} // namespace blender::compositor

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_string_ref.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Implicit Input Operation
*
* An operation that outputs a result representing a specific implicit input. */
class ImplicitInputOperation : public Operation {
private:
/* The identifier of the output. */
static const StringRef output_identifier_;
/* The type of implicit input needed. */
ImplicitInputType implicit_input_;
public:
ImplicitInputOperation(Context &context, const ImplicitInputType implicit_input);
void execute() override;
/* Get a reference to the output result of the operation, this essentially calls the super
* get_result with the output identifier of the operation. */
Result &get_result();
/* Returns the domain of the implicit input. If the implicit input is a single value,
* std::nullopt is returned. */
static std::optional<Domain> get_domain(const Context &context,
const ImplicitInputType implicit_input);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,70 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <optional>
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Input Realization Mode
*
* Specifies how the input should be realized before execution. See the discussion in COM_domain.hh
* for more information on what realization mean. */
enum class InputRealizationMode : uint8_t {
/* The input should not be realized in any way. */
None,
/* The rotation and scale transforms of the input should be realized. */
Transforms,
/* The input should be realized on the operation domain, noting that the operation domain have
* its transforms realized. */
OperationDomain,
};
/* ------------------------------------------------------------------------------------------------
* Implicit Input Type
*
* Specifies the implicit input type that should be assigned to the input if it is unlinked. See
* the ImplicitInputOperation operation for more information on the individual types. */
enum class ImplicitInputType : uint8_t {
/* The input should have the uniform image coordinates of the compositing space as an input. */
UniformImageCoordinates,
/* The input should have the scene frame as an input. */
SceneFrame,
};
/* ------------------------------------------------------------------------------------------------
* Input Descriptor
*
* A class that describes an input of an operation. */
class InputDescriptor {
public:
/* The type of input. This may be different that the type of result that the operation will
* receive for the input, in which case, an implicit conversion operation will be added as an
* input processor to convert it to the required type. */
ResultType type;
/* Specify how the input should be realized. */
InputRealizationMode realization_mode = InputRealizationMode::OperationDomain;
/* If not nullopt, specifies the type of implicit input in case the input in unlinked. */
std::optional<ImplicitInputType> implicit_input = std::nullopt;
/* The priority of the input for determining the operation domain. The non-single value input
* with the highest priority will be used to infer the operation domain, the highest priority
* being zero. See the discussion in COM_domain.hh for more information. */
int domain_priority = 0;
/* If true, the input expects a single value, and if a non-single value is provided, a default
* single value will be used instead, see the get_<type>_value_default methods in the Result
* class. It follows that this also implies no realization, because we don't need to realize a
* result that will be discarded anyways. If false, the input can work with both single and
* non-single values. */
bool expects_single_value = false;
/* If true, the input will not be implicitly converted to the type of the input and will be
* passed as is. */
bool skip_type_conversion = false;
};
} // namespace blender::compositor

View File

@@ -0,0 +1,44 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <string>
#include "BLI_map.hh"
#pragma once
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Cryptomatte Meta Data
*
* Stores the Cryptomatte meta data as specified in Section 1 "Metadata" in the Cryptomatte
* specification. The Cryptomatte layer name is not stored because it is determined by the user
* when saving the result to file. */
struct CryptomatteMetaData {
std::string hash;
std::string conversion;
std::string manifest;
};
/* ------------------------------------------------------------------------------------------------
* Meta Data
*
* Stores extra information about results such as image meta data that can eventually be saved to
* file. */
struct MetaData {
/* The result stores non color data, which is not to be color-managed. */
bool is_non_color_data = false;
/* Stores Cryptomatte meta data. This will only be initialized for results that represent
* Cryptomatte information. See the CryptomatteMetaData structure for more information. */
CryptomatteMetaData cryptomatte;
/* Stores custom meta-data fields. */
Map<std::string, std::string> fields;
/* Identifies if the result represents a Cryptomatte layer. This is identified based on whether
* the Cryptomatte meta data are initialized. */
bool is_cryptomatte_layer() const;
};
} // namespace blender::compositor

View File

@@ -0,0 +1,123 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "FN_multi_function_procedure.hh"
#include "FN_multi_function_procedure_builder.hh"
#include "FN_multi_function_procedure_executor.hh"
#include "NOD_multi_function.hh"
#include "COM_context.hh"
#include "COM_input_descriptor.hh"
#include "COM_pixel_operation.hh"
namespace blender::compositor {
struct Schedule;
/* ------------------------------------------------------------------------------------------------
* Multi-Function Procedure Operation
*
* A pixel operation that evaluates a multi-function procedure built from the pixel compile unit
* using the multi-function procedure builder, see FN_multi_function_procedure_builder.hh for more
* information. Also see the PixelOperation class for more information on pixel operations. */
class MultiFunctionProcedureOperation : public PixelOperation {
private:
/* The multi-function procedure, its builder, and executor that are backing the operation. This
* is created and compiled during construction. */
mf::Procedure procedure_;
mf::ProcedureBuilder procedure_builder_;
std::unique_ptr<mf::ProcedureExecutor> procedure_executor_;
/* A map that associates each node in the compile unit with an instance of its multi-function
* builder. */
Map<const bNode *, std::unique_ptr<nodes::NodeMultiFunctionBuilder>> node_multi_functions_;
/* A map that associates the output sockets of each node to the variables that were created for
* them. */
Map<const bNodeSocket *, mf::Variable *> output_to_variable_map_;
/* A map that associates implicit inputs to the variables that were created for them. */
Map<ImplicitInputType, mf::Variable *> implicit_input_to_variable_map_;
/* A vector that stores the intermediate variables that were implicitly created for the procedure
* but are not associated with a node output. Those variables are for such multi-functions like
* constant inputs and implicit conversion. */
Vector<mf::Variable *> implicit_variables_;
/* A set that stores the variables that are used as the outputs of the procedure. */
Set<mf::Variable *> output_variables_;
/* A vector that stores the identifiers of the parameters of the multi-function procedure in
* order. The parameters include both inputs and outputs. This is used to retrieve the input and
* output results for each of the parameters in the procedure. Note that parameters have no
* identifiers and are identified solely by their order. */
Vector<std::string> parameter_identifiers_;
public:
/* Build a multi-function procedure as well as an executor for it from the given pixel compile
* unit and execution schedule. If the operation is operating on single values, is_single_value
* should be true. */
MultiFunctionProcedureOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule,
const bool is_single_value,
const ComputeContext &compute_context);
/* Calls the multi-function procedure executor on the domain of the operator passing in the
* inputs and outputs as parameters. */
void execute() override;
private:
/* Builds the procedure by going over the nodes in the compile unit, calling their
* multi-functions and creating any necessary inputs or outputs to the operation/procedure. */
void build_procedure();
/* Get the variables corresponding to the inputs of the given node. The variables can be those
* that were returned by a previous call to a multi-function, those that were generated as
* constants for unlinked inputs, or those that were added as inputs to the operation/procedure
* itself. The variables are implicitly converted to the type expected by the multi-function. */
Vector<mf::Variable *> get_input_variables(const bNode &node,
const mf::MultiFunction &multi_function);
/* Returns a constant variable that was created by calling a constant function carrying the value
* of the given input socket. */
mf::Variable *get_constant_input_variable(const bNodeSocket &input);
/* Given an unlinked input with an implicit input. Declare an input to the operation/procedure
* for that implicit input if not done already and return a variable that represent that implicit
* input. */
mf::Variable *get_implicit_input_variable(const bNodeSocket &input);
/* Given an input in a node that is part of the compile unit that is connected to an output that
* is in a non that is not part of the compile unit. Declare an input to the operation/procedure
* for that output if not done already and return a variable that represent that input. */
mf::Variable *get_multi_function_input_variable(const bNodeSocket &input_socket,
const bNodeSocket &output_socket);
/* Given the variables that were returned by calling the multi-function for the given node,
* assign the variables to their corresponding outputs. And if an output is connected to a node
* outside of the compile unit or is used as the preview of the node, declare an output to the
* operation/procedure for it. */
void assign_output_variables(const bNode &node, Vector<mf::Variable *> &variables);
/* Populate an output to the operator/procedure for the given output socket whose value is stored
* in the given variable. The variable is implicitly converted to the type expected by the
* socket. */
void populate_operation_result(const bNodeSocket &output_socket, mf::Variable *variable);
/* Convert the given variable to the given expected type. This is done by adding an implicit
* conversion function whose output variable will be returned. If no conversion is needed, the
* given variable is returned as is. If conversion is not possible, a fallback default variable
* will b returned. */
mf::Variable *convert_variable(mf::Variable *variable, const mf::DataType expected_type);
/* Creates and returns a variable that carries the default value of the given type. */
mf::Variable *get_default_value_variable(const mf::DataType type);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,168 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include "BLI_enum_flags.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "COM_compile_state.hh"
#include "COM_context.hh"
#include "COM_node_operation.hh"
#include "COM_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Enumerates the possible node group outputs can be computed. Those can be combined into a bit
* flag. */
enum class NodeGroupOutputTypes : uint8_t {
None = 0,
GroupOutputNode = 1 << 0,
ViewerNode = 1 << 1,
FileOutputNode = 1 << 2,
NodePreviews = 1 << 3,
};
ENUM_OPERATORS(NodeGroupOutputTypes)
/* ------------------------------------------------------------------------------------------------
* Node Group Operation
*
* The node group operation represents and evaluates a node group. It compiles the node group into
* an operations stream, evaluating the operations in the process. It should be noted that
* operations are eagerly evaluated as soon as they are compiled, as opposed to compiling the whole
* operations stream and then evaluating it in a separate step. This is done because the evaluator
* uses the evaluated results of previously compiled operations to compile the operations that
* follow them in an optimized manner.
*
* Evaluation starts by computing an optimized node execution schedule by calling the
* compute_schedule function, see the discussion in COM_scheduler.hh for more details. For the node
* tree shown below, the execution schedule is denoted by the node numbers. The compiler then goes
* over the execution schedule in order and compiles each node into either a Node Operation or a
* Pixel Operation, depending on the node type, see the is_pixel_node function. A pixel operation
* is constructed from a group of nodes forming a contiguous subset of the node execution schedule.
* For instance, in the node tree shown below, nodes 3 and 4 are compiled together into a pixel
* operation and node 5 is compiled into its own pixel operation, both of which are contiguous
* subsets of the node execution schedule. This process is described in details in the following
* section.
*
* Pixel Operation 1 Pixel Operation 2
* +-----------------------------------+ +------------------+
* .------------. | .------------. .------------. | | .------------. | .------------.
* | Node 1 | | | Node 3 | | Node 4 | | | | Node 5 | | | Node 6 |
* | |----|--| |--| |---|-----|--| |--|--| |
* | | .-|--| | | | | .--|--| | | | |
* '------------' | | '------------' '------------' | | | '------------' | '------------'
* | +-----------------------------------+ | +------------------+
* .------------. | |
* | Node 2 | | |
* | |--'----------------------------------------'
* | |
* '------------'
*
* For non pixel nodes, the compilation process is straight forward, the compiler instantiates a
* node operation from the node, map its inputs to the results of the outputs they are linked to,
* and evaluates the operations. However, for pixel nodes, since a group of nodes can be compiled
* together into a pixel operation, the compilation process is a bit involved. The compiler uses
* an instance of the Compile State class to keep track of the compilation process. The compiler
* state stores the so called "pixel compile unit", which is the current group of nodes that will
* eventually be compiled together into a pixel operation. While going over the schedule, the
* compiler adds the pixel nodes to the compile unit until it decides that the compile unit is
* complete and should be compiled. This is typically decided when the current node is not
* compatible with the compile unit and can't be added to it, only then it compiles the compile
* unit into a pixel operation and resets it to ready it to track the next potential group of
* nodes that will form a pixel operation. This decision is made based on various criteria in the
* should_compile_pixel_compile_unit function. See the discussion in COM_compile_state.hh for more
* details of those criteria, but perhaps the most evident of which is whether the node is actually
* a pixel node, if it isn't, then it evidently can't be added to the compile unit and the compile
* unit is should be compiled.
*
* For the node tree above, the compilation process is as follows. The compiler goes over the node
* execution schedule in order considering each node. Nodes 1 and 2 are not pixel node so they are
* compiled into node operations and added to the operations stream. The current compile unit is
* empty, so it is not compiled. Node 3 is a pixel node, and since the compile unit is currently
* empty, it is unconditionally added to it. Node 4 is a pixel node, it was decided---for the sake
* of the demonstration---that it is compatible with the compile unit and can be added to it. Node
* 5 is a pixel node, but it was decided---for the sake of the demonstration---that it is not
* compatible with the compile unit, so the compile unit is considered complete and is compiled
* first, adding the first pixel operation to the operations stream and resetting the compile
* unit. Node 5 is then added to the now empty compile unit similar to node 3. Node 6 is not a
* pixel node, so the compile unit is considered complete and is compiled first, adding the first
* pixel operation to the operations stream and resetting the compile unit. Finally, node 6 is
* compiled into a node operation similar to nodes 1 and 2 and added to the operations stream. */
class NodeGroupOperation : public Operation {
private:
/* The node group that this operation represents. */
const bNodeTree &node_group_;
/* The node group outputs that should be computed. See NodeGroupOutputTypes for more details. */
const NodeGroupOutputTypes needed_output_types_;
/* The node instance key of the active node group. This could be this node group or a child of
* it. In case of the former, this will be equal to instance_key_. */
const bNodeInstanceKey active_node_group_instance_key_ = bke::NODE_INSTANCE_KEY_BASE;
/* A node instance key that identifies the particular group node that uses this node group. If
* this node group operation represents a top-level standalone node group with no associated
* group node, this will be bke::NODE_INSTANCE_KEY_BASE. */
const bNodeInstanceKey instance_key_ = bke::NODE_INSTANCE_KEY_BASE;
/* A compute context that identifies the particular group node that uses this node group or the
* scene for the top-level compositor node tree. */
const ComputeContext &compute_context_;
/* The compiled operations stream, which contains all compiled operations so far. */
Vector<std::unique_ptr<Operation>> operations_stream_;
public:
/* Populate the output results based on the node group interface outputs and populate the input
* descriptors based on the node group interface inputs. */
NodeGroupOperation(Context &context,
const bNodeTree &node_group,
const NodeGroupOutputTypes needed_outputs,
const bNodeInstanceKey active_node_group_instance_key,
const bNodeInstanceKey instance_key,
const ComputeContext &compute_context);
/* Compile and evaluate the node group. */
void execute() override;
private:
/* Compile the given node into a node operation, map each input to the result of the output
* linked to it, update the compile state, add the newly created operation to the operations
* stream, and evaluate the operation. */
void evaluate_node(const bNode &node, CompileState &compile_state);
/* Constructs and returns a node operation that represents to the given node. */
NodeOperation *get_node_operation(const bNode &node);
/* Map each input of the node operation to the result of the output linked to it. Unlinked inputs
* are mapped to the result of a newly created Input Single Value Operation, which is added to
* the operations stream and evaluated. Since this method might add operations to the operations
* stream, the actual node operation should only be added to the stream once this method is
* called. */
void map_node_operation_inputs_to_their_results(const bNode &node,
NodeOperation *operation,
CompileState &compile_state);
/* Compile the pixel compile unit into a pixel operation, map each input of the operation to
* the result of the output linked to it, update the compile state, add the newly created
* operation to the operations stream, evaluate the operation, and finally reset the pixel
* compile unit. */
void evaluate_pixel_compile_unit(CompileState &compile_state);
/* Map each input of the pixel operation to the result of the output linked to it. This might
* also correct the reference counts of the results, see the implementation for more details. */
void map_pixel_operation_inputs_to_their_results(PixelOperation *operation,
CompileState &compile_state);
/* Cancels the evaluation by freeing the results of the operations that were already evaluated,
* that's because later operations that use the already allocated results will not be evaluated,
* so they consequently will not release the results that they use and we need to free them
* manually. */
void cancel_evaluation();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
struct Schedule;
/* ------------------------------------------------------------------------------------------------
* Node Operation
*
* A node operation is a subclass of operation that nodes should implement and instantiate in the
* get_compositor_operation function of bNodeType, passing the inputs given to that function to the
* constructor. This class essentially just implements a default constructor that populates output
* results for all outputs of the node as well as input descriptors for all inputs of the nodes
* based on their socket declaration. The class also provides some utility methods for easier
* implementation of nodes. */
class NodeOperation : public Operation {
private:
/* The node that this operation represents. */
const bNode &node_;
/* A node instance key that identifies the node instance in the nested node groups path. */
bNodeInstanceKey instance_key_ = bke::NODE_INSTANCE_KEY_NONE;
/* The compute context where this node operation is executing. */
const ComputeContext *compute_context_ = nullptr;
/* False if node previews are not needed and true otherwise. */
bool needs_node_previews_ = false;
public:
/* Populate the output results based on the node outputs and populate the input descriptors based
* on the node inputs. */
NodeOperation(Context &context, const bNode &node);
/* Calls the evaluate method of the operation, but also measures the execution time and stores it
* in the context's profile data. */
void evaluate() override;
/* Compute and set the initial reference counts of all the results of the operation. The
* reference counts of the results are the number of operations that use those results, which is
* computed as the number of inputs whose node is part of the schedule and is linked to the
* output corresponding to each result. The node execution schedule is given as an input. */
void compute_results_reference_counts(const Schedule &schedule);
/* Setter and getter for instance_key_. */
void set_instance_key(const bNodeInstanceKey &instance_key);
const bNodeInstanceKey &get_instance_key() const;
/* Setter and getter for compute_context_. */
void set_compute_context(const ComputeContext &compute_context);
const ComputeContext &get_compute_context() const;
/* Setter for needs_node_previews_. */
void set_needs_node_previews(const bool needed);
protected:
/* Log the values for the inputs and outputs of the node as well as its image preview. */
void log_data() override;
/* Returns a reference to the node that this operation represents. */
const bNode &node() const;
private:
/* Get the result which will be previewed in the node, this is chosen as the first linked output
* of the node, if no outputs exist, then the first allocated input will be chosen. Returns
* nullptr if no result is viewable. */
Result *get_preview_result();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,153 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
namespace blender::compositor {
class SimpleOperation;
/* A type representing a vector of simple operations that store the input processors for a
* particular input. */
using ProcessorsVector = Vector<std::unique_ptr<SimpleOperation>>;
/* ------------------------------------------------------------------------------------------------
* Operation
*
* The operation is the basic unit of the compositor. The evaluator compiles the compositor node
* tree into an ordered stream of operations which are then executed in order during evaluation.
* The operation class can be sub-classed to implement a new operation. Operations have a number of
* inputs and outputs that are declared during construction and are identified by string
* identifiers. Inputs are declared by calling declare_input_descriptor providing an appropriate
* descriptor. Those inputs are mapped to the results computed by other operations whose outputs
* are linked to the inputs. Such mappings are established by the compiler during compilation by
* calling the map_input_to_result method. Outputs are populated by calling the populate_result
* method, providing a result of an appropriate type. Upon execution, the operation allocates a
* result for each of its outputs and computes their value based on its inputs and options.
*
* Each input may have one or more input processors, which are simple operations that process the
* inputs before the operation is executed, see the discussion in COM_simple_operation.hh for more
* information. And thus the effective input of the operation is the result of the last input
* processor if one exists. Input processors are added and evaluated by calling the
* evaluate_input_processors method, which provides a default implementation that does things like
* implicit conversion, domain realization, and more. This default implementation can, however, be
* overridden, extended, or removed.
*
* The operation is evaluated by calling the evaluate method, which first evaluates the input
* processors, then it calls the execute method of the operation, and finally it releases the
* results mapped to the inputs to declare that they are no longer needed. */
class Operation {
private:
/* A reference to the compositor context. This member references the same object in all
* operations but is included in the class for convenience. */
Context &context_;
/* A mapping between each output of the operation identified by its identifier and the result for
* that output. A result for each output of the operation should be constructed and added to the
* map during operation construction by calling the populate_result method. The results should be
* allocated and their contents should be computed in the execute method. */
Map<std::string, Result> results_;
/* A mapping between each input of the operation identified by its identifier and its input
* descriptor. Those descriptors should be declared during operation construction by calling the
* declare_input_descriptor method. */
Map<std::string, InputDescriptor> input_descriptors_;
/* A mapping between each input of the operation identified by its identifier and a pointer to
* the computed result providing its data. The mapped result is either one that was computed by
* another operation or one that was internally computed in the operation by the last input
* processor for that input. It is the responsibility of the evaluator to map the inputs to their
* linked results before evaluating the operation by calling the map_input_to_result method. */
Map<StringRef, Result *> results_mapped_to_inputs_;
/* A mapping between each input of the operation identified by its identifier and an ordered list
* of simple operations to process that input. */
Map<StringRef, ProcessorsVector> input_processors_;
public:
Operation(Context &context);
virtual ~Operation();
virtual void evaluate();
/* Get a reference to the result connected to the input identified by the given identifier. */
Result &get_input(StringRef identifier) const;
/* Get a reference to the output result identified by the given identifier. */
Result &get_result(StringRef identifier);
/* Map the input identified by the given identifier to the result providing its data. See
* results_mapped_to_inputs_ for more details. This should be called by the evaluator to
* establish links between different operations. */
void map_input_to_result(StringRef identifier, Result *result);
/* Free the results of the operation. Note that normally, operation results aren't freed by the
* operation itself, but by the operations that consume those results, see the release_inputs
* method. But this is used to force free results in cases like canceled evaluations where later
* operations will not get evaluated and thus will not free the results it consumes. */
void free_results();
protected:
/* Compute the operation domain of this operation. By default, this implements a default logic
* that infers the operation domain from the inputs, which may be overridden for a different
* logic. See the discussion in COM_domain.hh for the inference logic and more information. */
virtual Domain compute_domain();
/* Add and evaluate any needed input processors, which essentially just involves calling the
* add_and_evaluate_input_processor method with the needed processors. This is called before
* executing the operation to prepare its inputs. The class defines a default implementation
* which adds typically needed processors, but derived classes can override the method to have
* a different implementation, extend the implementation, or remove it entirely. */
virtual void evaluate_input_processors();
/* This method should allocate the operation results, execute the operation, and compute the
* output results. */
virtual void execute() = 0;
/* Log the data of the operation into the context logger. */
virtual void log_data();
/* Add a new result with the given type to the results_ map identified by the given output
* identifier. This should be called during operation construction for all outputs. */
void populate_result(StringRef identifier, ResultType type);
/* Declare the descriptor of the input identified by the given identifier to be the given
* descriptor. Adds the given descriptor to the input_descriptors_ map identified by the given
* input identifier. This should be called during operation constructor for all inputs. */
void declare_input_descriptor(StringRef identifier, InputDescriptor descriptor);
/* Get a reference to the descriptor of the input identified by the given identified. */
InputDescriptor &get_input_descriptor(StringRef identifier);
/* Allocates all needed outputs that are not yet allocated and default initialize them. */
void allocate_default_remaining_outputs();
/* Returns a reference to the compositor context. */
Context &context() const;
private:
/* Given the identifier of an input of the operation and a processor operation:
* - Add the given processor to the list of input processors for the input.
* - Map the input of the processor to be the result of the last input processor or the result
* mapped to the input if no previous processors exists.
* - Switch the result mapped to the input to be the output result of the processor.
* - Evaluate the processor. */
void add_and_evaluate_input_processor(StringRef identifier, SimpleOperation *processor);
/* Release the results that are mapped to the inputs of the operation. This is called after the
* evaluation of the operation to declare that the results are no longer needed by this
* operation. */
void release_inputs();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,160 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
namespace blender::compositor {
struct Schedule;
/* A type representing a contiguous subset of the node execution schedule that will be compiled
* into a Pixel Operation. */
using PixelCompileUnit = VectorSet<const bNode *>;
/* ------------------------------------------------------------------------------------------------
* Pixel Operation
*
* An operation that is evaluated pixel-wise and is compiled from a contiguous subset of the node
* execution schedule, whose nodes all represent pixel-wise operations. The subset of the node
* execution schedule is called a Pixel Compile Unit and contains nodes that are called Pixel
* nodes, see the discussion in COM_compile_state.hh for more information. Since the nodes inside
* the compile unit are all pixel wise, they can be combined into a single operation that can be
* evaluated more efficiently. This is an abstract class that should be implemented to compile and
* evaluate the compile unit as needed.
*
* Consider the following node graph with a node execution schedule denoted by the number on each
* node. The compiler may decide to compile a subset of the execution schedule into a pixel
* operation if they are all pixel nodes, in this case, the nodes from 3 to 5 were compiled
* together into a pixel operation. This subset is called the pixel compile unit. See the
* discussion in COM_evaluator.hh for more information on the compilation process. Links that are
* internal to the pixel operation are established between the input and outputs of the pixel
* nodes, for instance, the links between nodes 3 and 4 as well as those between nodes 4 and 5.
* However, links that cross the boundary of the pixel operation needs special handling.
*
* Pixel Operation
* +------------------------------------------------------+
* .------------. | .------------. .------------. .------------. | .------------.
* | Node 1 | | | Node 3 | | Node 4 | | Node 5 | | | Node 6 |
* | |----|--| |--| |------| |--|--| |
* | | .-|--| | | | .---| | | | |
* '------------' | | '------------' '------------' | '------------' | '------------'
* | +----------------------------------|-------------------+
* .------------. | |
* | Node 2 | | |
* | |--'------------------------------------'
* | |
* '------------'
*
* Links from nodes that are not part of the pixel operation to nodes that are part of the pixel
* operation are considered inputs of the operation itself and are declared as such. For instance,
* the link from node 1 to node 3 is declared as an input to the operation, and the same applies
* for the links from node 2 to nodes 3 and 5. Note, however, that only one input is declared for
* each distinct output socket, so both links from node 2 share the same input of the operation.
*
* Links from nodes that are part of the pixel operation to nodes that are not part of the pixel
* operation are considered outputs of the operation itself and are declared as such. For instance,
* the link from node 5 to node 6 is declared as an output to the operation. */
class PixelOperation : public Operation {
protected:
/* The compile unit that will be compiled into this pixel operation. */
PixelCompileUnit compile_unit_;
/* A reference to the node execution schedule that is being compiled. */
const Schedule &schedule_;
/* The compute context where this pixel operation is executing. */
const ComputeContext &compute_context_;
/* False if node previews are not needed and true otherwise. */
bool needs_node_previews_ = false;
/* A map that associates the identifier of each input of the operation with the output socket it
* is linked to. This is needed to help the compiler establish links between operations. */
Map<std::string, const bNodeSocket *> inputs_to_linked_outputs_map_;
/* A map that associates the output socket of a node that is not part of the pixel operation to
* the identifier of the input of the operation that was declared for it. */
Map<const bNodeSocket *, std::string> outputs_to_declared_inputs_map_;
/* A map that associates each of the needed implicit inputs with the identifiers of the inputs of
* the operation that were declared for them. */
Map<ImplicitInputType, std::string> implicit_inputs_to_input_identifiers_map_;
/* A map that associates the identifier of each input of the operation with the number of node
* inputs that use it, that is, its reference count. This is needed to correct the reference
* counts of results linked to the inputs of the operation, since the results that provide the
* inputs aren't aware that multiple of their outgoing links are now part of a single pixel
* operation. For instance, if an output is linked to both inputs of a Math node, its computed
* reference count would be 2, but the pixel operation of the Math node would only create a
* single shared input for it, so from the point of view of the evaluator, the reference count
* should actually be 1. So the result's reference count should be corrected by decrementing it
* by the internal reference count computed in this map minus 1. */
Map<std::string, int> inputs_to_reference_counts_map_;
/* A map that associates the output socket that provides the result of an output of the operation
* with the identifier of that output. This is needed to help the compiler establish links
* between operations. */
Map<const bNodeSocket *, std::string> output_sockets_to_output_identifiers_map_;
/* A vector set that stores all output sockets that are used as previews for nodes inside the
* pixel operation. */
VectorSet<const bNodeSocket *> preview_outputs_;
/* A vector set that stores all output sockets that will be logged to the node evaluator log. */
VectorSet<const bNodeSocket *> logged_outputs_;
/* True if the operation operates on single values, that is, all of its inputs and outputs are
* single values. */
const bool is_single_value_;
public:
PixelOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule,
const ComputeContext &compute_context,
const bool is_single_value);
/* Log the values of all inputs and outputs of nodes inside the pixel operation as well as node
* previews for all nodes in the pixel operations if the node requires a preview.
*
* Previews are computed from results that are populated for outputs that are used to compute
* previews even if they are internally linked, and those outputs are stored and tracked in the
* preview_outputs_ vector set, see the populate_results_for_node method for more information. */
void log_data() override;
/* Get the identifier of the operation output corresponding to the given output socket. This is
* called by the compiler to identify the operation output that provides the result for an input
* by providing the output socket that the input is linked to. See
* output_sockets_to_output_identifiers_map_ for more information. */
StringRef get_output_identifier_from_output_socket(const bNodeSocket &output_socket);
/* Get a reference to the inputs to linked outputs map of the operation. This is called by the
* compiler to identify the output that each input of the operation is linked to for correct
* input mapping. See inputs_to_linked_outputs_map_ for more information. */
Map<std::string, const bNodeSocket *> &get_inputs_to_linked_outputs_map();
/* Get a reference to the implicit inputs to input identifiers map of the operation. This is
* called by the compiler to link the operations inputs with their corresponding implicit input
* results. See implicit_inputs_to_input_identifiers_map_ for more information. */
Map<ImplicitInputType, std::string> &get_implicit_inputs_to_input_identifiers_map();
/* Returns the internal reference count of the operation input with the given identifier. See the
* inputs_to_reference_counts_map_ member for more information. */
int get_internal_input_reference_count(const StringRef &identifier);
/* Compute and set the initial reference counts of all the results of the operation. The
* reference counts of the results are the number of operations that use those results, which is
* computed as the number of inputs linked to the output corresponding to each of the results of
* the operation, but only the linked inputs whose node is part of the schedule but not part of
* the pixel operation, since inputs that are part of the pixel operations are internal links.
*
* Additionally, results that are used as node previews gets an extra reference count because
* they are referenced and released by the compute_preview method.
*
* The node execution schedule is given as an input. */
void compute_results_reference_counts(const Schedule &schedule);
/* Setter for needs_node_previews_. */
void set_needs_node_previews(const bool needed);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,60 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
#include "COM_simple_operation.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Realize On Domain Operation
*
* A simple operation that projects the input on a certain target domain, copies the area of the
* input that intersects the target domain, and fill the rest with the extension options in the
* realization options of the input. See the discussion in COM_domain.hh for more information. */
class RealizeOnDomainOperation : public SimpleOperation {
private:
/* The target domain to realize the input on. */
Domain target_domain_;
public:
RealizeOnDomainOperation(Context &context, Domain target_domain, ResultType type);
void execute() override;
/* Determine if a realize on domain operation is needed for the input with the given result and
* descriptor in an operation with the given operation domain. If it is not needed, return a null
* pointer. If it is needed, return an instance of the operation.
*
* Since operations might not be transform-invariant, the rotation and scale components of the
* operation domain are realized and the size of the domain is increased/reduced to adapt to the
* new transformation. For instance, if the transformation is a rotation, the domain will be
* rotated and expanded in size to account for the bounding box of the domain after rotation. */
static SimpleOperation *construct_if_needed(Context &context,
const Result &input_result,
const InputDescriptor &input_descriptor,
const Domain &operation_domain);
protected:
/* The operation domain is just the target domain. */
Domain compute_domain() override;
private:
/* Get the name of the realization shader of the appropriate type. */
const char *get_realization_shader_name();
/* Computes the translation that the input should be translated by to fix the artifacts related
* to interpolation. See the implementation for more information. */
float2 compute_corrective_translation();
void realize_on_domain_gpu(const float3x3 &transformation);
void realize_on_domain_cpu(const float3x3 &transformation);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,133 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_scene_types.h"
namespace blender {
struct RenderResult;
namespace compositor {
class Result;
/* ------------------------------------------------------------------------------------------------
* File Output
*
* A FileOutput represents an image that will be saved to a file output. The image is internally
* stored as a RenderResult and saved at the path according to the image format. The image can
* either be saved as an EXR image or a non-EXR image, specified by the format. This is important
* because EXR images needs to constructed differently from other image types as will be explained
* in the following sections.
*
* For EXR images, the render result needs to be composed of passes for each layer, so the add_pass
* method should be called to add each of the passes. Additionally, an empty view should be added
* for each of the views referenced by the passes, using the single-argument overload of the
* add_view method. Those views are merely empty structure and does not hold any data aside from
* the view name. An exception to this rule is stereo EXR images, which needs to have the same
* structure as non-EXR images as explained in the following section.
*
* For non-EXR images, the render result needs to composed of views, so the multi-argument overload
* of the method add_view should be used to add each view.
*
* Color management will be applied on the images if save_as_render_ is true.
*
* Meta data can be added using the add_meta_data function. */
class FileOutput {
private:
std::string path_;
ImageFormatData format_;
RenderResult *render_result_;
bool save_as_render_;
Map<std::string, std::string> meta_data_;
public:
/* Allocate and initialize the internal render result of the file output using the give
* parameters. See the implementation for more information. */
FileOutput(const std::string &path,
const ImageFormatData &format,
int2 size,
bool save_as_render);
/* Free the internal render result. */
~FileOutput();
/* Add an empty view with the given name. An empty view is just structure and does not hold any
* data aside from the view name. This should be called for each view referenced by passes. This
* should only be called for EXR images. */
void add_view(const char *view_name);
/* Add a view of the given name that stores the given result data. */
void add_view(const char *view_name, const Result &data);
/* Add a pass of the given name in the given view that stores the given result data composed of
* each of the channels given by the channels string. The channels string should contain a
* character for each channel in the pixel buffer representing the channel ID. This should only
* be called for EXR images. The given view name should be the name of an added view using the
* add_view method. */
void add_pass(const char *pass_name,
const char *view_name,
const char *channels,
const Result &data);
/* Add meta data that will eventually be saved to the file if the format supports it. */
void add_meta_data(std::string key, std::string value);
/* Save the file to the path along with its meta data, reporting any reports to the standard
* output. */
void save(Scene *scene);
};
/* ------------------------------------------------------------------------------------------------
* Render Context
*
* A render context is created by the render pipeline and passed to the compositor to stores data
* that is specifically related to the rendering process. In particular, since the compositor is
* executed for each view separately and consecutively, it can be used to store and accumulate
* data from each of the evaluations of each view, for instance, to save all views in a single file
* for the File Output node, see the file_outputs_ member for more information. */
class RenderContext {
public:
/* True if the render context represents an animation render. */
bool is_animation_render = false;
private:
/* A mapping between file outputs and their image file paths. Those are constructed in the
* get_file_output method and saved in the save_file_outputs method. See those methods for more
* information. */
Map<std::string, std::unique_ptr<FileOutput>> file_outputs_;
public:
/* Check if there is an available file output with the given path in the context, if one exists,
* return it, otherwise, return a newly created one from the given parameters and add it to the
* context. The arguments are ignored if the file output already exist. This method is typically
* called in the File Output nodes in the compositor.
*
* Since the compositor gets executed multiple times for each view, for single view renders, the
* file output will be constructed and fully initialized in the same compositor evaluation. For
* multi-view renders, the file output will be constructed in the evaluation of the first view,
* and each view will subsequently add its data until the file output is fully initialized in the
* last view. The render pipeline code will then call the save_file_outputs method after all
* views were evaluated to write the file outputs. */
FileOutput &get_file_output(std::string path,
ImageFormatData format,
int2 size,
bool save_as_render);
/* Write the file outputs that were added to the context. The render pipeline code should call
* this method after all views were evaluated to write the file outputs. See the get_file_output
* method for more information. */
void save_file_outputs(Scene *scene);
};
} // namespace compositor
} // namespace blender

View File

@@ -0,0 +1,780 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <variant>
#include "BLI_assert.h"
#include "BLI_color_types.hh"
#include "BLI_compiler_compat.h"
#include "BLI_cpp_type.hh"
#include "BLI_generic_pointer.hh"
#include "BLI_generic_span.hh"
#include "BLI_implicit_sharing_ptr.hh"
#include "BLI_math_interp.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_vector.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "BLI_string_ref.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "NOD_menu_value.hh"
#include "COM_domain.hh"
#include "COM_meta_data.hh"
namespace blender {
struct Object;
struct Image;
struct VFont;
struct Scene;
struct Text;
struct Mask;
} // namespace blender
namespace blender::compositor {
class Context;
class DerivedResources;
/* Make sure to update the format related static methods in the Result class. */
enum class ResultType : uint8_t {
Float,
Float2,
Float3,
Float4,
Color,
Int,
Int2,
Int3,
Int4,
Bool,
Float4x4,
Menu,
Quaternion,
/* Single value only types. See Result::is_single_value_only_type. */
String,
Object,
Image,
Font,
Scene,
Text,
Mask,
};
/* The precision of the data. CPU data is always stored using full precision at the moment. */
enum class ResultPrecision : uint8_t {
Full,
Half,
};
/* The type of storage used to hold the result data. */
enum class ResultStorageType : uint8_t {
/* Stored as a gpu::Texture on the GPU. */
GPU,
/* Stored as a buffer on the CPU. */
CPU,
};
using Color = ColorSceneLinear4f<eAlpha::Premultiplied>;
/* ------------------------------------------------------------------------------------------------
* Result
*
* A result represents the computed value of an output of an operation. A result can either
* represent an image or a single value. A result is typed, and can be of types like color, vector,
* or float. Single value results are stored in 1x1 textures to make them easily accessible in
* shaders. But the same value is also stored in the value union member of the result for any
* host-side processing. The GPU texture of the result can either be allocated from the texture
* pool of the context referenced by the result or it can be allocated directly from the GPU
* module, see the allocation method for more information.
*
* Results are reference counted and their data are released once their reference count reaches
* zero. After constructing a result, the set_reference_count method is called to declare the
* number of operations that needs this result. Once each operation that needs the result no longer
* needs it, the release method is called and the reference count is decremented, until it reaches
* zero, where the result's data is then released.
*
* A result not only represents an image, but also the area it occupies in the virtual compositing
* space. This area is called the Domain of the result, see the discussion in COM_domain.hh for
* more information.
*
* The result data can be shared by multiple results or shared with some external entity outside of
* the compositor. This is achieved by managing the data in an ImplicitSharingInfo, which is heap
* allocated and shared by all results that share the same data. See the sharing_info_ and the
* share_data method for more information.
*
* A result may store resources that are computed and cached in case they are needed by multiple
* operations. Those are called Derived Resources and can be accessed using the derived_resources
* method. */
class Result {
private:
/* The context that the result was created within, this should be initialized during
* construction. */
Context *context_ = nullptr;
/* The base type of the result's image or single value. */
ResultType type_ = ResultType::Float;
/* The precision of the result's data. Only relevant for GPU textures. CPU buffers and single
* values are always stored using full precision. */
ResultPrecision precision_ = ResultPrecision::Half;
/* If true, the result is a single value, otherwise, the result is an image. */
bool is_single_value_ = false;
/* The type of storage used to hold the data. Used to correctly interpret the data union. */
ResultStorageType storage_type_ = ResultStorageType::GPU;
/* Stores a reference to the result's pixel data managed by the sharing info, either stored in a
* GPU texture or a buffer that is wrapped in a GSpan on CPU. This will represent a 1x1 image if
* the result is a single value, the value of which will be identical to that of the value
* member. See class description for more information. */
union {
/* This will be a 2D texture for most types, but can be a 2D texture array for large types like
* float4x4 where each column will be stored in a layer. */
gpu::Texture *gpu_texture_ = nullptr;
GSpan cpu_data_;
};
/* Implicit sharing info manages the result's data, allowing it to be shared between multiple
* results and eventually freeing it when it is no longer needed. It is heap allocated during
* data allocation, it gains new users through calls to share_data, and its users get removed
* and its data potentially deleted in the free method. Notice that implicit sharing in Blender
* allows copying shared data to make it mutable, this is not allowed in the compositor, and it
* does not implement a copy-on-write mechanism, so copying needs to be done explicitly. The
* result may contain data with a nullptr sharing info, this is a special case where the data is
* considered external and needn't be managed/freed by the result. */
ImplicitSharingPtr<> sharing_info_ = nullptr;
/* The number of users that currently needs this result. Operations initializes this by calling
* the set_reference_count method before evaluation. Once each operation that needs the result no
* longer needs it, the release method is called and the reference count is decremented, until it
* reaches zero, where the result's data is then released. */
int reference_count_ = 1;
/* If the result is a single value, this member stores the value of the result, the value of
* which will be identical to that stored in the data_ member. The active variant member depends
* on the type of the result. This member is uninitialized and should not be used if the result
* is not a single value. */
std::variant<float,
float2,
float3,
float4,
Color,
int32_t,
int2,
int3,
int4,
bool,
float4x4,
nodes::MenuValue,
math::Quaternion,
std::string,
Object *,
Image *,
VFont *,
Scene *,
Text *,
Mask *>
single_value_ = 0.0f;
/* The domain of the result. This only matters if the result was not a single value. See the
* discussion in COM_domain.hh for more information. */
Domain domain_ = Domain::identity();
/* Stores resources that are derived from this result. Lazily allocated if needed. See the class
* description for more information. */
DerivedResources *derived_resources_ = nullptr;
public:
/* Stores extra information about the result such as image meta data that can eventually be
* written to file. */
MetaData meta_data;
/* Construct a result within the given context. */
Result(Context &context);
/* Construct a result of the given type and precision within the given context. */
Result(Context &context, ResultType type, ResultPrecision precision);
/* Construct a result of an appropriate type and precision based on the given GPU texture format
* within the given context. */
Result(Context &context, gpu::TextureFormat format);
/* Returns true if the given type can only be used with single value results. Consequently, it is
* always allocated on the CPU and GPU code paths needn't support the type. */
static bool is_single_value_only_type(ResultType type);
/* Returns the appropriate GPU texture format based on the given result type and precision. A
* special case is given to Float3 and Int3, because 3-component textures can't be used as write
* targets in shaders, so we need to allocate 4-component textures for them, and ignore the
* fourth channel during processing. */
static gpu::TextureFormat gpu_texture_format(ResultType type, ResultPrecision precision);
/* Returns the GPU data format that corresponds to the give result type. */
static eGPUDataFormat gpu_data_format(const ResultType type);
/* Returns the GPU texture format that corresponds to the give one, but whose precision is the
* given precision. */
static gpu::TextureFormat gpu_texture_format(gpu::TextureFormat format,
ResultPrecision precision);
/* Returns the precision of the given GPU texture format. */
static ResultPrecision precision(gpu::TextureFormat format);
/* Returns the type of the given GPU texture format. */
static ResultType type(gpu::TextureFormat format);
/* Returns the CPP type corresponding to the given result type. */
static const CPPType &cpp_type(const ResultType type);
/* Returns a string representation of the given result type. */
static const char *type_name(const ResultType type);
/* Implicit conversion to the internal GPU texture. */
operator gpu::Texture *() const;
/* Returns the CPP type of the result. */
const CPPType &get_cpp_type() const;
/* Returns the appropriate texture format based on the result's type and precision. This is
* identical to the gpu_texture_format static method. This will match the format of the allocated
* texture, with one exception. Results of type Float3 or Int3 that share external textures might
* hold a 3-component texture as opposed to a 4-component one, which would have been created by
* uploading data from CPU. */
gpu::TextureFormat get_gpu_texture_format() const;
/* Identical to gpu_data_format but assumes the result's type. */
eGPUDataFormat get_gpu_data_format() const;
/* Declare the result to be a texture result, allocate a texture of an appropriate type with
* the size of the given domain, and set the domain of the result to the given domain.
*
* See the allocate_data method for more information on the from_pool and storage_type
* parameters. */
void allocate_texture(const Domain domain,
const bool from_pool = true,
const std::optional<ResultStorageType> storage_type = std::nullopt);
/* Declare the result to be a single value result, allocate a texture of an appropriate type with
* size 1x1 from the texture pool, and set the domain to be an identity domain. The value is zero
* initialized. See class description for more information. */
void allocate_single_value();
/* Allocate a single value result whose value is zero. This is called for results whose value
* can't be computed and are considered invalid. */
void allocate_invalid();
/* Creates and allocates a new result that matches the type and precision of this result and
* uploads the CPU data that exist in this result. The result is assumed to be allocated on the
* CPU. See the allocate_data method for more information on the from_pool parameters. */
Result upload_to_gpu(const bool from_pool) const;
/* Creates and allocates a new result that matches the type and precision of this result and
* downloads the GPU data that exist in this result. The result is assumed to be allocated on the
* GPU. */
Result download_to_cpu() const;
/* Bind the GPU texture of the result to the texture image unit with the given name in the
* currently bound given shader. This also inserts a memory barrier for texture fetches to ensure
* any prior writes to the texture are reflected before reading from it. */
void bind_as_texture(gpu::Shader *shader, const char *texture_name) const;
/* Bind the GPU texture of the result to the image unit with the given name in the currently
* bound given shader. If read is true, a memory barrier will be inserted for image reads to
* ensure any prior writes to the images are reflected before reading from it. */
void bind_as_image(gpu::Shader *shader, const char *image_name, bool read = false) const;
/* Unbind the GPU texture which was previously bound using bind_as_texture. */
void unbind_as_texture() const;
/* Unbind the GPU texture which was previously bound using bind_as_image. */
void unbind_as_image() const;
/* Share the data of the given source result. This is done by simply adding a new user to the
* sharing info of the result. The source data is expect to be allocated and have the same type
* and precision as this result. */
void share_data(const Result &source);
/* Share the data of a GPU texture that is managed by the given implicit sharing info. If no
* implicit sharing info is provided, the texture is assumed to be external, has a lifetime that
* covers the entire evaluation of the compositor, and will thus not be freed. The domain will be
* set to have the data and display size as the texture size. The given texture should have a
* format that is compatible with the result. */
void share_data(gpu::Texture *texture, ImplicitSharingPtr<> sharing_info = nullptr);
/* Share the data of a GPU buffer that is managed by the given implicit sharing info. If no
* implicit sharing info is provided, the buffer is assumed to be external, has a lifetime that
* covers the entire evaluation of the compositor, and will thus not be freed. The domain will be
* set to have the data and display size as the given size. The given buffer should have a format
* that is compatible with the result. */
void share_data(const void *data, int2 size, ImplicitSharingPtr<> sharing_info = nullptr);
/* Sets the transformation of the domain of the result to the given transformation. */
void set_transformation(const float3x3 &transformation);
/* Transform the result by the given transformation. This effectively pre-multiply the given
* transformation by the current transformation of the domain of the result. */
void transform(const float3x3 &transformation);
/* Get a reference to the realization options of this result. See the RealizationOptions struct
* for more information. */
RealizationOptions &get_realization_options();
const RealizationOptions &get_realization_options() const;
/* Set the value of reference_count_, see that member for more details. This should be called
* after constructing the result to declare the number of operations that needs it. */
void set_reference_count(int count);
/* Decrement the reference count of the result by the given count. */
void decrement_reference_count(int count = 1);
/* Decrement the reference count of the result and free its data if it reaches zero. */
void release();
/* Remove a user from the result's data and frees the data if there are no more owners. If the
* result is not allocated, this will do nothing. */
void free();
/* Returns true if this result should be computed and false otherwise. The result should be
* computed if its reference count is not zero, that is, its result is used by at least one
* operation. */
bool should_compute();
/* Returns a reference to the derived resources of the result, which is allocated if it was not
* allocated already. */
DerivedResources &derived_resources();
/* Returns the type of the result. */
ResultType type() const;
/* Returns the precision of the result. */
ResultPrecision precision() const;
/* Sets the type of the result. */
void set_type(ResultType type);
/* Sets the precision of the result. */
void set_precision(ResultPrecision precision);
/* Returns true if the result is a single value and false of it is an image. */
bool is_single_value() const;
/* Returns true if the result is allocated. */
bool is_allocated() const;
/* Returns the reference count of the result. */
int reference_count() const;
/* Returns a reference to the domain of the result. See the Domain class. */
Domain &domain();
const Domain &domain() const;
/* Computes the number of channels of the result based on its type. */
int64_t channels_count() const;
/* Computes the size of the result's data in bytes. */
int64_t size_in_bytes() const;
gpu::Texture *gpu_texture() const;
GSpan cpu_data() const;
GMutableSpan cpu_data_for_write();
const ImplicitSharingPtr<> &sharing_info() const;
/* It is important to call update_single_value_data after adjusting the single value. See that
* method for more information. */
GPointer single_value() const;
GMutablePointer single_value();
/* Gets the single value stored in the result. Assumes the result stores a value of the given
* template type. */
template<typename T> const T &get_single_value() const;
/* Gets the single value stored in the result, if the result is not a single value, the default
* identity value of the type is returned. Assumes the result stores a value of the same type as
* the template type. */
template<typename T> T get_single_value_default() const;
/* Sets the single value of the result to the given value, which also involves setting the single
* pixel in the image to that value. See the class description for more information. Assumes
* the result stores a value of the given template type. */
template<typename T> void set_single_value(const T &value);
/* Updates the single pixel in the image to the current single value in the result. This is
* called implicitly in the set_single_value method, but calling this explicitly is useful when
* the single value was adjusted through its data pointer returned by the single_value method.
* See the class description for more information. */
void update_single_value_data();
/* Loads the pixel at the given texel coordinates. Assumes the result stores a value of the given
* template type. If the CouldBeSingleValue template argument is true and the result is a single
* value result, then that single value is returned for all texel coordinates. */
template<typename T, bool CouldBeSingleValue = false> T load_pixel(const int2 &texel) const;
/* Identical to load_pixel but with the specified boundary extensions. */
template<typename T, bool CouldBeSingleValue = false>
T load_pixel(const int2 &texel,
const Extension &extension_mode_x,
const Extension &extension_mode_y) const;
/* Identical to load_pixel but with extended boundary extension. */
template<typename T, bool CouldBeSingleValue = false>
T load_pixel_extended(const int2 &texel) const;
/* Identical to load_pixel but with a fallback value for out of bound access. */
template<typename T, bool CouldBeSingleValue = false>
T load_pixel_fallback(const int2 &texel, const T &fallback) const;
/* Identical to load_pixel but with zero boundary extension. */
template<typename T, bool CouldBeSingleValue = false> T load_pixel_zero(const int2 &texel) const;
/* Stores the given pixel value in the pixel at the given texel coordinates. Assumes the result
* stores a value of the given template type. */
template<typename T> void store_pixel(const int2 &texel, const T &pixel_value);
/* Samples the result at the given normalized coordinates with the given interpolation and
* boundary extension. The interpolation is ignored for non float types that do not support
* interpolation. The jacobian represents the change of the given coordinates across space, if
* provided, the function will do area sampling for the area spanned by the jacobian, but if not
* provided, standard point sampling will be done. Assumes the result stores a value of the given
* template type. If the CouldBeSingleValue template argument is true and the result is a single
* value result, then that single value is returned for all coordinates. */
template<typename T, bool CouldBeSingleValue = false>
T sample(const float2 &coordinates,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
std::optional<float2x2> jacobian = std::nullopt) const;
/* Shorthand for sample() with bilinear interpolation and zero boundary extension. */
template<typename T, bool CouldBeSingleValue = false>
T sample_bilinear_zero(const float2 &coordinates) const;
/* Shorthand for sample() with bilinear interpolation and extended boundary extension. */
template<typename T, bool CouldBeSingleValue = false>
T sample_bilinear_extended(const float2 &coordinates) const;
private:
/* Allocates the image data for the given size.
*
* The data is allocated on the CPU or GPU depending on the given storage_type. A nullopt may be
* passed to storage_type, in which case, the data will be allocated on the device of the
* result's context as specified by context.use_gpu().
*
* If from_pool is true, GPU textures will be allocated from the texture pool of the context,
* otherwise, a new texture will be allocated. Pooling should not be used for persistent results
* that might span more than one evaluation, like cached resources. While pooling should be used
* for most other cases where the result will be allocated then later released in the same
* evaluation. Some types do not support pooling, since they require array textures which are not
* supported by the texture pool. */
void allocate_data(const int2 size,
const bool from_pool = true,
const std::optional<ResultStorageType> storage_type = std::nullopt);
/* Same as get_pixel_index but can be used when the type of the result is not known at compile
* time. */
int64_t get_pixel_index(const int2 &texel) const;
};
StringRefNull to_string(const ResultPrecision &precision);
/* -------------------------------------------------------------------- */
/* Inline Methods.
*/
BLI_INLINE_METHOD Domain &Result::domain()
{
return domain_;
}
BLI_INLINE_METHOD const Domain &Result::domain() const
{
return domain_;
}
BLI_INLINE_METHOD gpu::Texture *Result::gpu_texture() const
{
BLI_assert(storage_type_ == ResultStorageType::GPU);
return gpu_texture_;
}
BLI_INLINE_METHOD GSpan Result::cpu_data() const
{
BLI_assert(storage_type_ == ResultStorageType::CPU);
return cpu_data_;
}
inline const ImplicitSharingPtr<> &Result::sharing_info() const
{
return sharing_info_;
}
BLI_INLINE_METHOD GMutableSpan Result::cpu_data_for_write()
{
BLI_assert(storage_type_ == ResultStorageType::CPU);
BLI_assert(sharing_info_ && sharing_info_->is_mutable());
return GMutableSpan(cpu_data_.type(), const_cast<void *>(cpu_data_.data()), cpu_data_.size());
}
template<typename T> BLI_INLINE_METHOD const T &Result::get_single_value() const
{
BLI_assert(this->is_single_value());
return std::get<T>(single_value_);
}
template<typename T> BLI_INLINE_METHOD T Result::get_single_value_default() const
{
if (this->is_single_value()) {
return this->get_single_value<T>();
}
return *static_cast<const T *>(this->get_cpp_type().default_value());
}
template<typename T> BLI_INLINE_METHOD void Result::set_single_value(const T &value)
{
BLI_assert(this->is_allocated());
BLI_assert(this->is_single_value());
single_value_ = value;
this->update_single_value_data();
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::load_pixel(const int2 &texel) const
{
if constexpr (CouldBeSingleValue) {
if (is_single_value_) {
return this->get_single_value<T>();
}
}
else {
BLI_assert(!this->is_single_value());
}
return this->cpu_data().typed<T>()[this->get_pixel_index(texel)];
}
BLI_INLINE math::InterpWrapMode map_extension_mode_to_wrap_mode(const Extension &mode)
{
switch (mode) {
case Extension::Clip:
return math::InterpWrapMode::Border;
case Extension::Repeat:
return math::InterpWrapMode::Repeat;
case Extension::Extend:
return math::InterpWrapMode::Extend;
}
BLI_assert_unreachable();
return math::InterpWrapMode::Border;
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::load_pixel(const int2 &texel,
const Extension &extension_mode_x,
const Extension &extension_mode_y) const
{
if constexpr (CouldBeSingleValue) {
if (is_single_value_) {
return this->get_single_value<T>();
}
}
else {
BLI_assert(!this->is_single_value());
}
const math::InterpWrapMode wrap_mode_x = map_extension_mode_to_wrap_mode(extension_mode_x);
const math::InterpWrapMode wrap_mode_y = map_extension_mode_to_wrap_mode(extension_mode_y);
const int x = wrap_coord(texel.x, domain_.data_size.x, wrap_mode_x);
const int y = wrap_coord(texel.y, domain_.data_size.y, wrap_mode_y);
if (x < 0 || y < 0) {
return T{};
}
return this->load_pixel<T>(int2(x, y));
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::load_pixel_extended(const int2 &texel) const
{
if constexpr (CouldBeSingleValue) {
if (is_single_value_) {
return this->get_single_value<T>();
}
}
else {
BLI_assert(!this->is_single_value());
}
const int2 clamped_texel = math::clamp(texel, int2(0), domain_.data_size - int2(1));
return this->cpu_data().typed<T>()[this->get_pixel_index(clamped_texel)];
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::load_pixel_fallback(const int2 &texel, const T &fallback) const
{
if constexpr (CouldBeSingleValue) {
if (is_single_value_) {
return this->get_single_value<T>();
}
}
else {
BLI_assert(!this->is_single_value());
}
if (texel.x < 0 || texel.y < 0 || texel.x >= domain_.data_size.x ||
texel.y >= domain_.data_size.y)
{
return fallback;
}
return this->cpu_data().typed<T>()[this->get_pixel_index(texel)];
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::load_pixel_zero(const int2 &texel) const
{
return this->load_pixel_fallback<T, CouldBeSingleValue>(texel, T(0));
}
template<typename T>
BLI_INLINE_METHOD void Result::store_pixel(const int2 &texel, const T &pixel_value)
{
this->cpu_data_for_write().typed<T>()[this->get_pixel_index(texel)] = pixel_value;
}
struct EWASamplingData {
const Result &result;
const Extension extension_mode_x;
const Extension extension_mode_y;
};
/* Given a result and its extension modes as the userdata argument with the type EWASamplingData,
* load the pixel at the given texel coordinates with the given extension modes and write the pixel
* to the result argument. */
static inline void sample_ewa_read_callback(void *userdata, int x, int y, float result[4])
{
const EWASamplingData *sampling_data = static_cast<const EWASamplingData *>(userdata);
const Color sampled_result = sampling_data->result.load_pixel<Color>(
int2(x, y), sampling_data->extension_mode_x, sampling_data->extension_mode_y);
copy_v4_v4(result, sampled_result);
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::sample(const float2 &coordinates,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
std::optional<float2x2> jacobian) const
{
if constexpr (CouldBeSingleValue) {
if (is_single_value_) {
return this->get_single_value<T>();
}
}
const int2 size = domain_.data_size;
const float2 texel_coordinates = coordinates * float2(size);
if constexpr (is_same_any_v<T, float, float2, float3, float4, Color, math::Quaternion>) {
T pixel_value;
const float *buffer = static_cast<const float *>(this->cpu_data().data());
float *output = nullptr;
if constexpr (std::is_same_v<T, float>) {
output = &pixel_value;
}
else {
output = pixel_value;
}
const math::InterpWrapMode wrap_mode_x = map_extension_mode_to_wrap_mode(extension_mode_x);
const math::InterpWrapMode wrap_mode_y = map_extension_mode_to_wrap_mode(extension_mode_y);
switch (interpolation) {
case Interpolation::Nearest:
math::interpolate_nearest_wrapmode_fl(buffer,
output,
size.x,
size.y,
sizeof(T) / sizeof(float),
texel_coordinates.x,
texel_coordinates.y,
wrap_mode_x,
wrap_mode_y);
break;
case Interpolation::Bilinear:
math::interpolate_bilinear_wrapmode_fl(buffer,
output,
size.x,
size.y,
sizeof(T) / sizeof(float),
texel_coordinates.x - 0.5f,
texel_coordinates.y - 0.5f,
wrap_mode_x,
wrap_mode_y);
break;
case Interpolation::Bicubic:
math::interpolate_cubic_bspline_wrapmode_fl(buffer,
output,
size.x,
size.y,
sizeof(T) / sizeof(float),
texel_coordinates.x - 0.5f,
texel_coordinates.y - 0.5f,
wrap_mode_x,
wrap_mode_y);
break;
case Interpolation::Anisotropic:
BLI_assert(type_ == ResultType::Color);
const float2 x_gradient = jacobian.has_value() ? jacobian.value()[0] :
float2(1.0f / size.x, 0.0f);
const float2 y_gradient = jacobian.has_value() ? jacobian.value()[1] :
float2(0.0f, 1.0f / size.y);
EWASamplingData sampling_data = EWASamplingData{*this, extension_mode_x, extension_mode_y};
BLI_ewa_filter(size.x,
size.y,
false,
true,
coordinates,
x_gradient,
y_gradient,
sample_ewa_read_callback,
&sampling_data,
output,
extension_mode_x == Extension::Clip && extension_mode_y == Extension::Clip);
break;
}
return pixel_value;
}
else {
return this->load_pixel<T>(int2(texel_coordinates), extension_mode_x, extension_mode_y);
}
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::sample_bilinear_zero(const float2 &coordinates) const
{
return this->sample<T, CouldBeSingleValue>(
coordinates, Interpolation::Bilinear, Extension::Clip, Extension::Clip);
}
template<typename T, bool CouldBeSingleValue>
BLI_INLINE_METHOD T Result::sample_bilinear_extended(const float2 &coordinates) const
{
return this->sample<T, CouldBeSingleValue>(
coordinates, Interpolation::Bilinear, Extension::Extend, Extension::Extend);
}
BLI_INLINE_METHOD int64_t Result::get_pixel_index(const int2 &texel) const
{
BLI_assert(!is_single_value_);
BLI_assert(this->is_allocated());
BLI_assert(texel.x >= 0 && texel.y >= 0 && texel.x < domain_.data_size.x &&
texel.y < domain_.data_size.y);
return int64_t(texel.y) * domain_.data_size.x + texel.x;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_set.hh"
#include "BLI_vector_set.hh"
#include "COM_context.hh"
#include "COM_node_group_operation.hh"
namespace blender::compositor {
struct Schedule {
VectorSet<const bNode *> nodes;
/* Holds the set of all inputs sockets that needn't be computed because the node does not need
* them, for instance, the unneeded inputs of a Switch node. */
Set<const bNodeSocket *> unneeded_inputs;
};
/* Computes the execution schedule of the node group with the given instance key, assuming the
* active node group has the given active instance key. Only outputs types and node group outputs
* that are need are computed. This is essentially a post-order depth first traversal of the node
* tree from the needed output nodes to the leaf input nodes, with informed order of traversal of
* dependencies based on a heuristic estimation of the number of needed buffers. */
Schedule compute_schedule(const Context &context,
const bNodeTree &node_group,
NodeGroupOperation &node_group_operation,
const NodeGroupOutputTypes needed_outputs_types,
const bNodeInstanceKey instance_key,
const bNodeInstanceKey active_node_group_instance_key);
} // namespace blender::compositor

View File

@@ -0,0 +1,63 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "DNA_node_types.h"
#include "GPU_material.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Shader Node
*
* A shader node encapsulates a compositor node tree that is capable of being used together with
* other shader nodes to construct a Shader Operation using the GPU material compiler. A GPU node
* stack for each of the node inputs and outputs is stored and populated during construction in
* order to represent the node as a GPU node inside the GPU material graph, see GPU_material.hh for
* more information. The compiler is expected to initialize the input links of the node inputs
* before invoking the compile method. See the discussion in COM_shader_operation.hh for more
* information. */
class ShaderNode {
private:
/* The node that this operation represents. */
const bNode &node_;
/* The GPU node stacks of the inputs of the node. Those are populated during construction in the
* populate_inputs method. The links of the inputs are initialized by the GPU material compiler
* prior to calling the compile method. There is an extra stack at the end to mark the end of the
* array, as this is what the GPU module functions expect. */
Vector<GPUNodeStack> inputs_;
/* The GPU node stacks of the outputs of the node. Those are populated during construction in the
* populate_outputs method. There is an extra stack at the end to mark the end of the array, as
* this is what the GPU module functions expect. */
Vector<GPUNodeStack> outputs_;
public:
/* Construct the node by populating both its inputs and outputs. */
ShaderNode(const bNode &node);
/* Compile the node by adding the appropriate GPU material graph nodes and linking the
* appropriate resources. */
void compile(GPUMaterial *material);
/* Returns the GPU node stack of the input with the given identifier. */
GPUNodeStack &get_input(StringRef identifier);
/* Returns the GPU node stack of the output with the given identifier. */
GPUNodeStack &get_output(StringRef identifier);
private:
/* Populate the inputs of the node. The input link is set to nullptr and is expected to be
* initialized by the GPU material compiler before calling the compile method. */
void populate_inputs();
/* Populate the outputs of the node. The output link is set to nullptr and is expected to be
* initialized by the compile method. */
void populate_outputs();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,202 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include "BLI_map.hh"
#include "BLI_vector_set.hh"
#include "GPU_material.hh"
#include "GPU_shader.hh"
#include "gpu_shader_create_info.hh"
#include "COM_context.hh"
#include "COM_input_descriptor.hh"
#include "COM_pixel_operation.hh"
#include "COM_shader_node.hh"
namespace blender::compositor {
struct Schedule;
/* ------------------------------------------------------------------------------------------------
* Shader Operation
*
* A pixel operation that evaluates a shader compiled from the pixel compile unit using the GPU
* material compiler, see GPU_material.hh for more information. Also see the PixelOperation class
* for more information on pixel operations.
*
* An input to the pixel operation is declared for a distinct output socket as follows:
*
* - A texture is added to the shader, which will be bound to the result of the output socket
* during evaluation.
* - A GPU attribute is added to the GPU material for that output socket and is linked to the GPU
* input stack of the inputs linked to the output socket.
* - Code is emitted to initialize the values of the attributes by sampling the textures
* corresponding to each of the inputs.
* - The newly added attribute is mapped to the output socket in output_to_material_attribute_map_
* to share that same attributes for all inputs linked to the same output socket.
*
* An output to the pixel operation is declared for an output socket as follows:
*
* - An image is added in the shader where the output value will be written.
* - A storer GPU material node that stores the value of the output is added and linked to the GPU
* output stack of the output. The storer will store the value in the image identified by the
* index of the output given to the storer.
* - The storer functions are generated dynamically to map each index with its appropriate image.
*
* The GPU material code generator source is used to construct a compute shader that is then
* dispatched during operation evaluation after binding the inputs, outputs, and any necessary
* resources. */
class ShaderOperation : public PixelOperation {
private:
/* The GPU material backing the operation. This is created and compiled during construction and
* freed during destruction. */
GPUMaterial *material_;
/* A map that associates each node in the compile unit with an instance of its shader node. */
Map<const bNode *, std::unique_ptr<ShaderNode>> shader_nodes_;
/* A map that associates the output socket of a node that is not part of the shader operation to
* the attribute that was created for it. This is used to share the same attribute with all
* inputs that are linked to the same output socket. */
Map<const bNodeSocket *, GPUNodeLink *> output_to_material_attribute_map_;
/* A map that associates implicit inputs to the attributes that were created for them. */
Map<ImplicitInputType, GPUNodeLink *> implicit_input_to_material_attribute_map_;
public:
/* Shaders operations have a limit on how many outputs and inputs they can support. Inputs use
* attributes which can't be more than 15 and outputs use images which can't be more than 8. */
static const int maximum_inputs_count = 15;
static const int maximum_outputs_count = 8;
/* Construct and compile a GPU material from the given shader compile unit and execution schedule
* by calling GPU_material_from_callbacks with the appropriate callbacks. */
ShaderOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule,
const ComputeContext &compute_context);
/* Free the GPU material. */
~ShaderOperation() override;
/* Allocate the output results, bind the shader and all its needed resources, then dispatch the
* shader. */
void execute() override;
private:
/* Bind the uniform buffer of the GPU material as well as any color band textures needed by the
* GPU material. The compiled shader of the material is given as an argument and assumed to be
* bound. */
void bind_material_resources(gpu::Shader *shader);
/* Bind the input results of the operation to the appropriate textures in the GPU material. The
* attributes stored in output_to_material_attribute_map_ have names that match the texture
* samplers in the shader as well as the identifiers of the operation inputs that they correspond
* to. The compiled shader of the material is given as an argument and assumed to be bound. */
void bind_inputs(gpu::Shader *shader);
/* Bind the output results of the operation to the appropriate images in the GPU material. The
* name of the images in the shader match the identifier of their corresponding outputs. The
* compiled shader of the material is given as an argument and assumed to be bound. */
void bind_outputs(gpu::Shader *shader);
/* A static callback method of interface ConstructGPUMaterialFn that is passed to
* GPU_material_from_callbacks to construct the GPU material graph. The thunk parameter will be a
* pointer to the instance of ShaderOperation that is being compiled. The method goes over the
* compile unit and does the following for each node:
*
* - Instantiate a ShaderNode from the node and add it to shader_nodes_.
* - Link the inputs of the node if needed. The inputs are either linked to other nodes in the
* GPU material graph or are exposed as inputs to the shader operation itself if they are
* linked to nodes that are not part of the shader operation.
* - Call the compile method of the shader node to actually add and link the GPU material graph
* nodes.
* - If any of the outputs of the node are linked to nodes that are not part of the shader
* operation, they are exposed as outputs to the shader operation itself. */
static void construct_material(void *thunk, GPUMaterial *material);
/* Link the inputs of the node if needed. Unlinked inputs will be linked to constant values. If
* the input is linked to a node that is not part of the shader operation, the input will be
* exposed as an input to the shader operation and linked to it. While if the input is linked to
* a node that is part of the shader operation, then it is linked to that node in the GPU
* material node graph. */
void link_node_inputs(const bNode &node);
/* Link the GPU stack of the given unavailable input to a constant zero value setter GPU node.
* The value is ignored since the socket is unavailable, but the GPU Material compiler expects
* all inputs to be linked, even unavailable ones. */
void link_node_input_unavailable(const bNodeSocket &input);
/* Link the GPU stack of the given unlinked input to a constant value setter GPU node that
* supplies the value of the unlinked input. */
void link_node_input_constant(const bNodeSocket &input);
/* Given an unlinked input with an implicit input. Declare a new input to the operation for that
* implicit input if not done already and link it to the input link of the GPU node stack of the
* input socket. */
void link_node_input_implicit(const bNodeSocket &input);
/* Given the input socket of a node that is part of the shader operation which is linked to the
* given output socket of a node that is also part of the shader operation, just link the output
* link of the GPU node stack of the output socket to the input link of the GPU node stack of the
* input socket. This essentially establishes the needed links in the GPU material node graph. */
void link_node_input_internal(const bNodeSocket &input_socket, const bNodeSocket &output_socket);
/* Given the input socket of a node that is part of the shader operation which is linked to the
* given output socket of a node that is not part of the shader operation, declare a new
* operation input and link it to the input link of the GPU node stack of the input socket. An
* operation input is only declared if no input was already declared for that same output socket
* before. */
void link_node_input_external(const bNodeSocket &input_socket, const bNodeSocket &output_socket);
/* Given the input socket of a node that is part of the shader operation which is linked to the
* given output socket of a node that is not part of the shader operation, declare a new input to
* the operation that is represented in the GPU material by a newly created GPU attribute. It is
* assumed that no operation input was declared for this same output socket before. In the
* generate_code_for_inputs method, a texture will be added in the shader for each of the
* declared inputs, having the same name as the attribute. Additionally, code will be emitted to
* initialize the attributes by sampling their corresponding textures. */
void declare_operation_input(const bNodeSocket &input_socket, const bNodeSocket &output_socket);
/* Populate the output results of the shader operation for output sockets of the given node that
* are linked to nodes outside of the shader operation or are used to compute a preview for the
* node. */
void populate_results_for_node(const bNode &node);
/* Given the output socket of a node that is part of the shader operation which is linked to an
* input socket of a node that is not part of the shader operation, declare a new output to the
* operation and link it to an output storer passing in the index of the output. In the
* generate_code_for_outputs method, an image will be added in the shader for each of the
* declared outputs. Additionally, code will be emitted to define the storer functions that store
* the value in the appropriate image identified by the given index. */
void populate_operation_result(const bNodeSocket &output_socket);
/* Inserts an implicit conversion function that converts from the type of the output to the type
* of the input if not already the same. We assume the input is already linked. */
void convert_input_link_type(const bNodeSocket &input, const bNodeSocket &output);
/* A static callback method of interface GPUCodegenCallbackFn that is passed to
* GPU_material_from_callbacks to create the shader create info of the GPU material. The thunk
* parameter will be a pointer to the instance of ShaderOperation that is being compiled.
*
* This method first generates the necessary code to load the inputs and store the outputs. Then,
* it creates a compute shader from the generated sources. Finally, it adds the necessary GPU
* resources to the shader. */
static void generate_code(void *thunk, GPUMaterial *material, GPUCodegenOutput *code_generator);
/* Add an image in the shader for each of the declared outputs. Additionally, emit code to define
* the storer functions that store the given value in the appropriate image identified by the
* given index. */
std::string generate_code_for_outputs(gpu::shader::ShaderCreateInfo &shader_create_info);
/* Add a texture will in the shader for each of the declared inputs/attributes in the operation,
* having the same name as the attribute. Additionally, emit code to initialize the attributes by
* sampling their corresponding textures. */
std::string generate_code_for_inputs(GPUMaterial *material,
gpu::shader::ShaderCreateInfo &shader_create_info);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,62 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Simple Operation
*
* A simple operation is an operation that takes exactly one input and computes exactly one output.
* Moreover, the output is guaranteed to only have a single user, that is, its reference count will
* be one. Such operations can be attached to the inputs of operations to pre-process the inputs to
* prepare them before the operation is executed. */
class SimpleOperation : public Operation {
private:
/* The identifier of the output. This is constant for all operations. */
static const StringRef output_identifier_;
/* The identifier of the input. This is constant for all operations. */
static const StringRef input_identifier_;
public:
using Operation::Operation;
/* Get a reference to the output result of the operation, this essentially calls the super
* get_result method with the output identifier of the operation. */
Result &get_result();
/* Map the input of the operation to the given result, this essentially calls the super
* map_input_to_result method with the input identifier of the operation. */
void map_input_to_result(Result *result);
protected:
/* Simple operations don't need input processors, so override with an empty implementation. */
void evaluate_input_processors() override;
/* Get a reference to the input result of the operation, this essentially calls the super
* get_result method with the input identifier of the operation. */
Result &get_input();
/* Populate the result of the operation, this essentially calls the super populate_result method
* with the output identifier of the operation and sets the initial reference count of the result
* to 1, since the result of an operation is guaranteed to have a single user. */
void populate_result(ResultType type);
/* Declare the descriptor of the input of the operation to be the given descriptor, this
* essentially calls the super declare_input_descriptor method with the input identifier of the
* operation. */
void declare_input_descriptor(InputDescriptor descriptor);
/* Get a reference to the descriptor of the input, this essentially calls the super
* get_input_descriptor method with the input identifier of the operation. */
InputDescriptor &get_input_descriptor();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_string_ref.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Single Value Node Input Operation
*
* An operation that outputs a single value result whose value is the value of an unlinked input
* socket. This is typically used to initialize the values of unlinked node input sockets. */
class SingleValueNodeInputOperation : public Operation {
private:
/* The identifier of the output. */
static const StringRef output_identifier_;
/* The input socket whose value will be computed as the operation's result. */
const bNodeSocket &input_socket_;
public:
SingleValueNodeInputOperation(Context &context, const bNodeSocket &input_socket);
/* Allocate a single value result and set its value to the default value of the input socket. */
void execute() override;
/* Get a reference to the output result of the operation, this essentially calls the super
* get_result with the output identifier of the operation. */
Result &get_result();
private:
/* Populate the result of the operation, this essentially calls the super populate_result method
* with the output identifier of the operation. */
void populate_result(ResultType type);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,80 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_bokeh_kernel.hh"
#include "COM_cached_image.hh"
#include "COM_cached_mask.hh"
#include "COM_cached_shader.hh"
#include "COM_deriche_gaussian_coefficients.hh"
#include "COM_distortion_grid.hh"
#include "COM_fog_glow_kernel.hh"
#include "COM_image_coordinates.hh"
#include "COM_keying_screen.hh"
#include "COM_morphological_distance_feather_weights.hh"
#include "COM_ocio_color_space_conversion_shader.hh"
#include "COM_smaa_precomputed_textures.hh"
#include "COM_string_image.hh"
#include "COM_symmetric_blur_weights.hh"
#include "COM_symmetric_separable_blur_weights.hh"
#include "COM_van_vliet_gaussian_coefficients.hh"
namespace blender::compositor {
/* -------------------------------------------------------------------------------------------------
* Static Cache Manager
*
* A static cache manager is a collection of cached resources that can be retrieved when needed and
* created if not already available. In particular, each cached resource type has its own instance
* of a container derived from the CachedResourceContainer type in the class. All instances of that
* cached resource type are stored and tracked in the container. See the CachedResource and
* CachedResourceContainer classes for more information.
*
* The manager deletes the cached resources that are no longer needed. A cached resource is said to
* be not needed when it was not used in the previous evaluation. This is done through the
* following mechanism:
*
* - After every evaluation, do the following:
* 1. All resources whose CachedResource::needed flag is false are deleted.
* 2. The CachedResource::needed flag of all remaining resources is set to false.
* - During evaluation, when retrieving any cached resource, set its CachedResource::needed flag to
* true.
*
* In effect, any resource that was not used in the previous evaluation will be deleted. This
* mechanism is implemented in the reset() method of the class, which should be called after every
* evaluation. */
class StaticCacheManager {
public:
SymmetricBlurWeightsContainer symmetric_blur_weights;
SymmetricSeparableBlurWeightsContainer symmetric_separable_blur_weights;
MorphologicalDistanceFeatherWeightsContainer morphological_distance_feather_weights;
CachedMaskContainer cached_masks;
SMAAPrecomputedTexturesContainer smaa_precomputed_textures;
OCIOColorSpaceConversionShaderContainer ocio_color_space_conversion_shaders;
OCIOToDisplayShaderContainer ocio_to_display_shaders;
DistortionGridContainer distortion_grids;
KeyingScreenContainer keying_screens;
CachedShaderContainer cached_shaders;
BokehKernelContainer bokeh_kernels;
CachedImageContainer cached_images;
DericheGaussianCoefficientsContainer deriche_gaussian_coefficients;
VanVlietGaussianCoefficientsContainer van_vliet_gaussian_coefficients;
FogGlowKernelContainer fog_glow_kernels;
ImageCoordinatesContainer image_coordinates;
StringImageContainer string_images;
public:
/* Reset the cache manager by deleting the cached resources that are no longer needed because
* they weren't used in the last evaluation and prepare the remaining cached resources to track
* their needed status in the next evaluation. See the class description for more information.
* This should be called before every evaluation. */
void reset();
/* Force free all resources even if they are still needed. This is useful to manually destroy the
* static cache manager, for instance, with a GPU context bound. */
void free();
};
} // namespace blender::compositor

View File

@@ -0,0 +1,16 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_node_operation.hh"
namespace blender::compositor {
/* Returns an instance of a new UndefinedNodeOperation for the given node. See the class for more
* information, */
NodeOperation *get_undefined_node_operation(Context &context, const bNode &node);
} // namespace blender::compositor

View File

@@ -0,0 +1,92 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <optional>
#include "BLI_index_range.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "DNA_node_types.h"
#include "GPU_shader.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Returns true if the socket is available and not virtual. Returns false otherwise. */
bool is_socket_available(const bNodeSocket *socket);
/* Get the output socket linked to the given node input. If the input is not linked to an output,
* a null output is returned. */
const bNodeSocket *get_output_linked_to_input(const bNodeSocket &input);
/* Get the result type that corresponds to the given socket data type. For vector sockets, the
* dimensions of the socket can be provided, but if not provided, 3 will be assumed. */
ResultType socket_data_type_to_result_type(const eNodeSocketDatatype data_type,
const std::optional<int> dimensions = std::nullopt);
/* Get the result type that corresponds to the type of the given socket. */
ResultType get_node_socket_result_type(const bNodeSocket *socket);
/* Get the result type that corresponds to the type of the given interface socket. */
ResultType get_node_interface_socket_result_type(const bNodeTreeInterfaceSocket &socket);
/* Returns true if any of the inputs linked to the given output satisfies the given condition,
* and false otherwise. */
bool is_output_linked_to_input_conditioned(const bNodeSocket &output,
FunctionRef<bool(const bNodeSocket &)> condition);
/* Returns the number of inputs linked to the given output that satisfy the given condition. */
int number_of_inputs_linked_to_output_conditioned(
const bNodeSocket &output, FunctionRef<bool(const bNodeSocket &)> condition);
/* A node is a pixel node if it defines a method to get a pixel node operation. */
bool is_pixel_node(const bNode &node);
/* Get the input descriptor of the given input socket. */
InputDescriptor input_descriptor_from_input_socket(const bNodeSocket *socket);
/* Get the input descriptor of the given interface input of the given node group. */
InputDescriptor input_descriptor_from_interface_input(const bNodeTree &node_group,
const bNodeTreeInterfaceSocket &socket);
/* Dispatch the given compute shader in a 2D compute space such that the number of threads in both
* dimensions is as small as possible but at least covers the entirety of threads_range assuming
* the shader has a local group size given by local_size. That means that the number of threads
* might be a bit larger than threads_range, so shaders has to put that into consideration. A
* default local size of 16x16 is assumed, which is the optimal local size for many image
* processing shaders. */
void compute_dispatch_threads_at_least(gpu::Shader *shader,
int2 threads_range,
int2 local_size = int2(16));
/* Returns true if a node preview needs to be computed for the give node. */
bool is_node_preview_needed(const bNode &node);
/* Returns the node output that will be used to generate previews. */
const bNodeSocket *find_preview_output_socket(const bNode &node);
/* -------------------------------------------------------------------- */
/* Inline Functions.
*/
/* Executes the given function in parallel over the given 2D range. The given function gets the
* texel coordinates of the element of the range as an argument. */
template<typename Function> inline void parallel_for(const int2 range, const Function &function)
{
threading::parallel_for(IndexRange(range.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(range.x)) {
function(int2(x, y));
}
}
});
}
} // namespace blender::compositor

View File

@@ -0,0 +1,18 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Computes and returns a lower resolution byte version of the given input after applying the
* appropriate color management specified in the given context. */
ImBuf *compute_preview(Context &context, const Result &input);
} // namespace blender::compositor

View File

@@ -0,0 +1,22 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Convolves the given color input by the given float or color kernel and write the result to the
* given output. If normalize_kernel is true, the kernel will be normalized such that it integrates
* to 1. The output will be allocated internally and is thus expected not to be previously
* allocated. */
void convolve(Context &context,
const Result &input,
const Result &kernel,
Result &output,
const bool normalize_kernel);
} // namespace blender::compositor

View File

@@ -0,0 +1,37 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Blur the input using a fourth order IIR filter approximating a Gaussian filter of the given
* sigma computed using Deriche's design method. This is based on the following paper:
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss. INRIA,
* 1993.
*
* This differs from the standard symmetric separable blur algorithm in that it is faster for high
* sigma values, the downside is that it consumes more memory and is only an approximation that
* might suffer from fringing and artifacts, though those are typically unnoticeable. This filter
* is numerically unstable and not accurate for sigma values larger than 32, in those cases, use
* the Van Vliet filter instead. Further, for sigma values less than 3, use direct convolution
* instead, since it is faster and more accurate. Neumann boundary is assumed.
*
* If extend_bounds is true, the input will be zero padded by radius amount of pixels.
*
* The output is written to the given output result, which will be allocated internally and is thus
* expected not to be previously allocated. */
void deriche_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &sigma,
const bool extend_bounds);
} // namespace blender::compositor

View File

@@ -0,0 +1,16 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Extracts the alpha channel from the given input and write it to the given output. The output
* will be allocated internally and is thus expected not to be previously allocated. */
void extract_alpha(Context &context, const Result &input, Result &output);
} // namespace blender::compositor

View File

@@ -0,0 +1,66 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Computes a jump flooding table from the given input and writes the result to the output. A jump
* flooding table computes for each pixel the texel location of the closest "seed pixel". A seed
* pixel is a pixel that is marked as such in the input, more on this later. This table is useful
* to compute a Voronoi diagram where the centroids are the seed pixels, it can be used to
* accurately approximate an euclidean distance transform, finally, it can be used to flood fill
* regions of an image.
*
* The input is expected to be initialized by the initialize_jump_flooding_value function. Seed
* pixels should specify true for the is_seed argument, and false otherwise. The texel input should
* be the texel location of the pixel. Both the input and output results should be of type
* ResultType::Int2.
*
* To compute a Voronoi diagram, the pixels lying at the centroid of the Voronoi cell should be
* marked as seed pixels. To compute an euclidean distance transform of a region or flood fill a
* region, the boundary pixels of the region should be marked as seed.
*
* The algorithm is based on the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Jump flooding in GPU with applications to Voronoi diagram
* and distance transform." Proceedings of the 2006 symposium on Interactive 3D graphics and
* games. 2006.
*
* But uses the more accurate 1+JFA variant from the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Variants of jump flooding algorithm for computing discrete
* Voronoi diagrams." 4th international symposium on voronoi diagrams in science and engineering
* (ISVD 2007). IEEE, 2007.*
*
* The algorithm is O(log2(n)) per pixel where n is the maximum dimension of the input, it follows
* that the execution time is independent of the number of the seed pixels. However, the developer
* should try to minimize the number of seed pixels because their number is proportional to the
* error of the algorithm as can be seen in "Figure 3: Errors of variants of JFA" in the variants
* paper. */
void jump_flooding(Context &context, Result &input, Result &output);
/* A special value that indicates that the pixel has not be flooded yet, and consequently is not a
* seed pixel. */
#define JUMP_FLOODING_NON_FLOODED_VALUE int2(-1)
/* Given the texel location of the closest seed pixel and whether the pixel is flooded, encode that
* information in an int2. */
inline int2 encode_jump_flooding_value(const int2 &closest_seed_texel, const bool is_flooded)
{
return is_flooded ? closest_seed_texel : JUMP_FLOODING_NON_FLOODED_VALUE;
}
/* Initialize the pixel at the given texel location for the algorithm as being seed or background.
* This essentially calls encode_jump_flooding_value with the texel location, because the pixel is
* the closest seed to itself. */
inline int2 initialize_jump_flooding_value(const int2 &texel, const bool is_seed)
{
return encode_jump_flooding_value(texel, is_seed);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,40 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include "BLI_math_filter.hh"
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Possible morphological operations to apply. */
enum class MorphologicalBlurOperation : uint8_t {
/* Dilate by taking the maximum from the original input and the blurred input. Which means the
* whites bleeds into the blacks while the blacks don't bleed into the whites. */
Dilate,
/* Erode by taking the minimum from the original input and the blurred input. Which means the
* blacks bleeds into the whites while the whites don't bleed into the blacks. */
Erode,
};
/* Applies a morphological blur on input using the given radius and filter type. This essentially
* applies a standard blur operation, but then takes the maximum or minimum from the original input
* and blurred input depending on the chosen operation, see the MorphologicalBlurOperation enum for
* more information. The output is written to the given output result, which will be allocated
* internally and is thus expected not to be previously allocated. */
void morphological_blur(
Context &context,
const Result &input,
Result &output,
const float2 &radius,
const MorphologicalBlurOperation operation = MorphologicalBlurOperation::Erode,
const math::FilterKernel filter_type = math::FilterKernel::Gauss);
} // namespace blender::compositor

View File

@@ -0,0 +1,21 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Dilate or erode the given input using a morphological operator with a circular structuring
* element of radius equivalent to the absolute value of the given distance parameter. A positive
* distance corresponds to dilate operator, while a negative distance corresponds to an erode
* operator. */
void morphological_distance(Context &context,
const Result &input,
Result &output,
const int distance);
} // namespace blender::compositor

View File

@@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "DNA_scene_types.h"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Dilate or erode the given input using a morphological inverse distance operation evaluated at
* the given falloff. The radius of the structuring element is equivalent to the absolute value of
* the given distance parameter. A positive distance corresponds to a dilate operator, while a
* negative distance corresponds to an erode operator. See the implementation and shader for more
* information. */
void morphological_distance_feather(Context &context,
const Result &input,
Result &output,
const int distance,
const int falloff_type = PROP_SMOOTH);
} // namespace blender::compositor

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Possible padding method to use. */
enum class PaddingMethod : uint8_t {
/* Pads with zeros. */
Zero,
/* Pads by extending the edge. */
Extend,
};
/* Pads the given input in both directions with the given size number of pixels. The output will be
* allocated internally and is thus expected not to be previously allocated. */
void pad(Context &context,
const Result &input,
Result &output,
const int2 size,
const PaddingMethod padding_method);
} // namespace blender::compositor

View File

@@ -0,0 +1,89 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Sum Reductions.
*/
/* Computes the sum of all pixels in the given input. */
float4 sum_color(Context &context, const Result &input);
/* Computes the sum of the logarithm of the luminance of all pixels in the given color input, using
* the given luminance coefficients to compute the luminance. */
float sum_log_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients);
/* --------------------------------------------------------------------
* Sum Of Squared Difference Reductions.
*/
/* Computes the sum of the squared difference between the all pixels in the given input and the
* given subtrahend. This can be used to compute the standard deviation if the given subtrahend is
* the mean. */
float4 sum_squared_difference_color(Context &context,
const Result &input,
const float4 subtrahend);
/* --------------------------------------------------------------------
* Minimum Reductions.
*/
/* Computes the minimum value of all pixels in the given input. */
float minimum_float(Context &context, const Result &input);
Color minimum_color(Context &context, const Result &input);
/* Computes the minimum luminance of all pixels in the given input, using the given luminance
* coefficients to compute the luminance. */
float minimum_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients);
/* Computes the minimum float of all pixels in the given float input, limited to the given range.
* Values outside of the given range are ignored. If non of the pixel values are in the range, the
* upper bound of the range is returned. For instance, if the given range is [-10, 10] and the
* image contains the values {-11, 2, 5}, the minimum will be 2, since -11 is outside of the range.
* This is particularly useful for Z Depth normalization, since Z Depth can contain near infinite
* values, so enforcing a lower bound is beneficial. */
float minimum_float_in_range(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound);
/* --------------------------------------------------------------------
* Maximum Reductions.
*/
/* Computes the maximum value of all pixels in the given input. */
float maximum_float(Context &context, const Result &input);
float2 maximum_float2(Context &context, const Result &input);
Color maximum_color(Context &context, const Result &input);
/* Computes the maximum luminance of all pixels in the given input, using the given luminance
* coefficients to compute the luminance. */
float maximum_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients);
/* Computes the maximum float of all pixels in the given float input, limited to the given range.
* Values outside of the given range are ignored. If non of the pixel values are in the range, the
* lower bound of the range is returned. For instance, if the given range is [-10, 10] and the
* image contains the values {2, 5, 11}, the maximum will be 5, since 11 is outside of the range.
* This is particularly useful for Z Depth normalization, since Z Depth can contain near infinite
* values, so enforcing an upper bound is beneficial. */
float maximum_float_in_range(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound);
} // namespace blender::compositor

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Blur the input using a recursive Gaussian blur algorithm given a certain radius. This differs
* from the standard symmetric separable blur algorithm in that it is orders of magnitude faster
* for very high radius value, the downside is that it consumes more memory and is only an
* approximation that might suffer from fringing and artifacts, though those are typically
* unnoticeable. Neumann boundary is assumed.
*
* If extend_bounds is true, the input will be zero padded by radius amount of pixels.
*
* The output is written to the given output result, which will be allocated internally and is thus
* expected not to be previously allocated. */
void recursive_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &radius,
const bool extend_bounds = false);
} // namespace blender::compositor

View File

@@ -0,0 +1,22 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Samples a pixel from a color result. */
Color sample_pixel(Context &context,
const Result &input,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
const float2 coordinates);
} // namespace blender::compositor

View File

@@ -0,0 +1,23 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Anti-alias the given input using the SMAA algorithm and write the result into the given output.
* See the SMAA_THRESHOLD, SMAA_LOCAL_CONTRAST_ADAPTATION_FACTOR, and SMAA_CORNER_ROUNDING defines
* in the implementation for information on the parameters. Only Float and Color result types are
* supported. */
void smaa(Context &context,
const Result &input,
Result &output,
const float threshold = 0.1f,
const float local_contrast_adaptation_factor = 2.0f,
const int corner_rounding = 25);
} // namespace blender::compositor

View File

@@ -0,0 +1,80 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Possible operations to apply on pixels before computing the summed area table. The Square
* operation, for instance, can be useful to compute image variance from sum of squares. */
enum class SummedAreaTableOperation : uint8_t {
Identity,
Square,
};
/* Computes a summed area table from the given input and write the table to the given output. A
* summed are table is an image where each pixel contains the sum of all pixels in the areas down
* and to its left toward the zero index, including the pixel itself. This table is particularly
* useful to accelerate filters that requires averaging large rectangular areas of the input, like
* a box filter. */
void summed_area_table(Context &context,
Result &input,
Result &output,
SummedAreaTableOperation operation = SummedAreaTableOperation::Identity);
/* Computes the sum of the rectangular region defined by the given lower and upper bounds from the
* given summed area table. It is assumed that the given upper bound is larger than the given lower
* bound, otherwise, undefined behavior is invoked. Looking at the diagram below, in order to
* compute the sum of area X, we sample the table at each of the corners of the area X, to get:
*
* Upper Right -> A + B + C + X (1)
* Upper Left -> A + B (2)
* Lower Right -> B + C (3)
* Lower Left -> B (4)
*
* We start from (1) and subtract (2) and (3) to get rid of A and C to get:
*
* (A + B + C + X) - (A + B) - (B + C) = (X - B)
*
* To get rid of B, we add (4) to get:
*
* (X - B) + B = X
*
* ^
* |
* +-------+-----+
* | | |
* | A | X |
* | | |
* +-------+-----+
* | | |
* | B | C |
* | | |
* o-------+-----+------>
*
* The aforementioned equation eliminates the edges between regions X, C, and A since they get
* subtracted with C and A. To avoid this, we subtract 1 from the lower bound and fallback to zero
* for out of bound sampling. */
inline float4 summed_area_table_sum(const Result &table,
const int2 &lower_bound,
const int2 &upper_bound)
{
int2 corrected_lower_bound = lower_bound - int2(1);
int2 corrected_upper_bound = math::min(table.domain().data_size - int2(1), upper_bound);
float4 addend = float4(table.load_pixel_zero<Color>(corrected_upper_bound)) +
float4(table.load_pixel_zero<Color>(corrected_lower_bound));
float4 subtrahend = float4(table.load_pixel_zero<Color>(
int2(corrected_lower_bound.x, corrected_upper_bound.y))) +
float4(table.load_pixel_zero<Color>(
int2(corrected_upper_bound.x, corrected_lower_bound.y)));
return addend - subtrahend;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,26 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_filter.hh"
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Blur the input using a horizontal and a vertical separable blur passes given a certain radius
* and filter type using SymmetricSeparableBlurWeights. If extend_bounds is true, the input will be
* zero padded by radius amount of pixels. The result is written to the given output, which will be
* allocated internally and is thus expected not to be previously allocated. */
void symmetric_separable_blur(Context &context,
const Result &input,
Result &output,
const float2 &radius,
const math::FilterKernel filter_type = math::FilterKernel::Gauss,
const bool extend_bounds = false);
} // namespace blender::compositor

View File

@@ -0,0 +1,34 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_filter.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Blur the input using a horizontal and a vertical separable blur passes given the filter type
* using SymmetricSeparableBlurWeights, where the number of weights is equal to weights_resolution.
* Since the radius can be variable, the number of weights can be less than or more than the number
* of pixels actually getting accumulated during blurring, so the weights are interpolated in the
* shader as needed, the resolution is typically set to the maximum possible radius if known. The
* radius of the blur can be variable and is defined using the given radius float image. The output
* is written to the given output result, which will be allocated internally and is thus expected
* not to be previously allocated.
*
* Technically, variable size blur can't be computed separably, however, assuming a sufficiently
* smooth radius field, the results can be visually pleasing, so this can be used a more performant
* variable size blur if the quality is satisfactory. */
void symmetric_separable_blur_variable_size(
Context &context,
const Result &input,
const Result &radius,
Result &output,
const int weights_resolution = 128,
const math::FilterKernel filter_type = math::FilterKernel::Gauss);
} // namespace blender::compositor

View File

@@ -0,0 +1,42 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* Blur the input using a fourth order IIR filter approximating a Gaussian filter of the given
* sigma computed using Van Vliet's design method. This is based on the following paper:
*
* Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
* filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
* 98EX170). Vol. 1. IEEE, 1998.
*
* However, we internally split the fourth order IIR filter into two second order sections in order
* to improve its numerical stability and improve parallelism. See the implementation for more
* information.
*
* This differs from the standard symmetric separable blur algorithm in that it is faster for high
* sigma values, the downside is that it consumes more memory and is only an approximation that
* might suffer from fringing and artifacts, though those are typically unnoticeable. This filter
* is not accurate for sigma values less than 32, in those cases, use the Deriche filter instead.
* Further, for sigma values less than 3, use direct convolution instead, since it is faster and
* more accurate. Neumann boundary is assumed.
*
* If extend_bounds is true, the input will be zero padded by radius amount of pixels.
*
* The output is written to the given output result, which will be allocated internally and is thus
* expected not to be previously allocated. */
void van_vliet_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &sigma,
const bool extend_bounds);
} // namespace blender::compositor

View File

@@ -0,0 +1,143 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_index_range.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "DNA_node_types.h"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "BKE_type_conversions.hh"
#include "GPU_shader.hh"
#include "GPU_state.hh"
#include "GPU_texture.hh"
#include "BKE_node.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_compute_preview.hh"
namespace blender::compositor {
static void compute_preview_cpu(Context &context, const Result &input, ImBuf *output)
{
const int2 input_size = input.domain().data_size;
const int2 preview_size = int2(output->x, output->y);
Result input_as_color = context.create_result(ResultType::Color);
if (input.type() == ResultType::Color) {
input_as_color.share_data(input);
}
else {
input_as_color.allocate_texture(input.domain());
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
conversions.convert_to_initialized_n(input.cpu_data(), input_as_color.cpu_data_for_write());
}
ColormanageProcessor color_processor = ColormanageProcessor::display_processor_new(
&context.get_scene().view_settings, &context.get_scene().display_settings);
uchar *data_dst = output->byte_data_for_write();
threading::parallel_for(IndexRange(preview_size.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(preview_size.x)) {
const int2 coordinates = int2((float2(x, y) / float2(preview_size)) * float2(input_size));
Color color = input_as_color.load_pixel<Color>(coordinates);
color_processor.apply_v4(color);
const int64_t index = (y * preview_size.x + x) * 4;
rgba_float_to_uchar(data_dst + index, color);
}
}
});
input_as_color.release();
}
static void compute_preview_gpu(Context &context, const Result &input_result, ImBuf *output)
{
const int2 preview_size = int2(output->x, output->y);
gpu::Shader *shader = context.get_shader("compositor_compute_preview");
GPU_shader_bind(shader);
if (input_result.type() == ResultType::Float) {
GPU_texture_swizzle_set(input_result, "rrr1");
}
input_result.bind_as_texture(shader, "input_tx");
Result preview_result = context.create_result(ResultType::Color);
preview_result.allocate_texture(Domain(preview_size));
preview_result.bind_as_image(shader, "preview_img");
compute_dispatch_threads_at_least(shader, preview_size);
input_result.unbind_as_texture();
preview_result.unbind_as_image();
GPU_shader_unbind();
/* Restore original swizzle mask set above. */
if (input_result.type() == ResultType::Float) {
GPU_texture_swizzle_set(input_result, "rgba");
}
GPU_memory_barrier(GPU_BARRIER_TEXTURE_FETCH);
float *preview_pixels = static_cast<float *>(
GPU_texture_read(preview_result, GPU_DATA_FLOAT, 0));
preview_result.release();
ColormanageProcessor color_processor = ColormanageProcessor::display_processor_new(
&context.get_scene().view_settings, &context.get_scene().display_settings);
uchar *data_dst = output->byte_data_for_write();
threading::parallel_for(IndexRange(preview_size.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(preview_size.x)) {
const int64_t index = (y * preview_size.x + x) * 4;
color_processor.apply_v4(preview_pixels + index);
rgba_float_to_uchar(data_dst + index, preview_pixels + index);
}
}
});
MEM_delete(preview_pixels);
}
/* Given the size of a result, compute a lower resolution size for a preview. The greater dimension
* will be assigned an arbitrarily chosen size of 128, while the other dimension will get the size
* that maintains the same aspect ratio. */
static int2 compute_preview_size(int2 size)
{
const int greater_dimension_size = 128;
if (size.x > size.y) {
return int2(greater_dimension_size, int(greater_dimension_size * (float(size.y) / size.x)));
}
return int2(int(greater_dimension_size * (float(size.x) / size.y)), greater_dimension_size);
}
ImBuf *compute_preview(Context &context, const Result &input)
{
const int2 preview_size = compute_preview_size(input.domain().data_size);
ImBuf *image_buffer = IMB_allocImBuf(UNPACK2(preview_size), ImBufFlags::ByteData);
if (context.use_gpu()) {
compute_preview_gpu(context, input, image_buffer);
}
else {
compute_preview_cpu(context, input, image_buffer);
}
return image_buffer;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,287 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <complex>
#include <numeric>
#include "BLI_array.hh"
#include "BLI_assert.h"
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_fftw.hh"
#include "BLI_index_range.hh"
#include "BLI_memory_utils.hh"
#include "BLI_task.hh"
#if defined(WITH_FFTW3)
# include <fftw3.h>
#endif
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_convolve.hh"
namespace blender::compositor {
void convolve(Context &context,
const Result &input,
const Result &kernel,
Result &output,
const bool normalize_kernel)
{
#if defined(WITH_FFTW3)
BLI_assert(input.type() == ResultType::Color);
BLI_assert(kernel.type() == ResultType::Float || kernel.type() == ResultType::Color);
BLI_assert(output.type() == ResultType::Color);
/* Since we will be doing a circular convolution, we need to zero pad the input image by the
* kernel size and vice versa to avoid the kernel affecting the pixels at the other side of
* image. The kernel size is limited by the image size since it will have no effect on the image
* during convolution. */
const int2 image_size = input.domain().data_size;
const int2 kernel_size = kernel.domain().data_size;
const int2 needed_padding_amount = math::max(kernel_size, image_size);
const int2 needed_spatial_size = image_size + needed_padding_amount - 1;
const int2 spatial_size = fftw::optimal_size_for_real_transform(needed_spatial_size);
/* The FFTW real to complex transforms utilizes the hermitian symmetry of real transforms and
* stores only half the output since the other half is redundant, so we only allocate half of
* the first dimension. See Section 4.3.4 Real-data DFT Array Format in the FFTW manual for
* more information. */
const int2 frequency_size = int2(spatial_size.x / 2 + 1, spatial_size.y);
constexpr int input_channels_count = 4;
const int64_t spatial_pixels_count = int64_t(spatial_size.x) * spatial_size.y;
const int64_t frequency_pixels_count = int64_t(frequency_size.x) * frequency_size.y;
/* A structure to gather all buffers that need to be forward transformed from the real to the
* frequency domain. */
struct ForwardTransformTask {
float *input;
std::complex<float> *output;
};
Vector<ForwardTransformTask> forward_transform_tasks;
/* Allocate a real buffer and a complex buffer for each of the input channels for the FFT input
* and output respectively, then add a forward transform task for it. */
Array<float *> image_spatial_domain_channels(input_channels_count);
Array<std::complex<float> *> image_frequency_domain_channels(input_channels_count);
for (const int channel : image_spatial_domain_channels.index_range()) {
image_spatial_domain_channels[channel] = fftwf_alloc_real(spatial_pixels_count);
image_frequency_domain_channels[channel] = reinterpret_cast<std::complex<float> *>(
fftwf_alloc_complex(frequency_pixels_count));
forward_transform_tasks.append(ForwardTransformTask{image_spatial_domain_channels[channel],
image_frequency_domain_channels[channel]});
}
BLI_SCOPED_DEFER([&]() {
for (const int channel : image_spatial_domain_channels.index_range()) {
fftwf_free(image_spatial_domain_channels[channel]);
fftwf_free(image_frequency_domain_channels[channel]);
}
});
const int kernel_channels_count = kernel.channels_count();
const bool is_color_kernel = kernel_channels_count == 4;
/* Allocate a real buffer and a complex buffer for each of the kernel channels for the FFT input
* and output respectively, then add a forward transform task for it. */
Array<float *> kernel_spatial_domain_channels(kernel_channels_count);
Array<std::complex<float> *> kernel_frequency_domain_channels(kernel_channels_count);
for (const int channel : kernel_spatial_domain_channels.index_range()) {
kernel_spatial_domain_channels[channel] = fftwf_alloc_real(spatial_pixels_count);
kernel_frequency_domain_channels[channel] = reinterpret_cast<std::complex<float> *>(
fftwf_alloc_complex(frequency_pixels_count));
forward_transform_tasks.append(ForwardTransformTask{
kernel_spatial_domain_channels[channel], kernel_frequency_domain_channels[channel]});
}
BLI_SCOPED_DEFER([&]() {
for (const int channel : kernel_spatial_domain_channels.index_range()) {
fftwf_free(kernel_spatial_domain_channels[channel]);
fftwf_free(kernel_frequency_domain_channels[channel]);
}
});
/* Create a real to complex and complex to real plans to transform the image to the frequency
* domain.
*
* Notice that FFTW provides an advanced interface as per Section 4.4.2 Advanced Real-data DFTs
* to transform all image channels simultaneously with interleaved pixel layouts. But profiling
* showed better performance when running a single plan in parallel for all image channels with a
* planner pixel format, so this is what we will be doing.
*
* The input and output buffers here are dummy buffers and still not initialized, because they
* are required by the planner internally for planning and their data will be overwritten. So
* make sure not to initialize the buffers before creating the plan. */
fftwf_plan forward_plan = fftwf_plan_dft_r2c_2d(
spatial_size.y,
spatial_size.x,
image_spatial_domain_channels[0],
reinterpret_cast<fftwf_complex *>(image_frequency_domain_channels[0]),
FFTW_ESTIMATE);
fftwf_plan backward_plan = fftwf_plan_dft_c2r_2d(
spatial_size.y,
spatial_size.x,
reinterpret_cast<fftwf_complex *>(image_frequency_domain_channels[0]),
image_spatial_domain_channels[0],
FFTW_ESTIMATE);
BLI_SCOPED_DEFER([&]() {
fftwf_destroy_plan(forward_plan);
fftwf_destroy_plan(backward_plan);
});
Result convolve_input = context.create_result(input.type());
Result convolve_kernel = context.create_result(kernel.type());
if (context.use_gpu()) {
Result input_cpu = input.download_to_cpu();
convolve_input.share_data(input_cpu);
input_cpu.release();
Result kernel_cpu = kernel.download_to_cpu();
convolve_kernel.share_data(kernel_cpu);
kernel_cpu.release();
}
else {
convolve_input.share_data(input);
convolve_kernel.share_data(kernel);
}
/* Zero pad the image to the required spatial domain size, storing each channel in planar
* format for better cache locality, that is, RRRR...GGGG...BBBB...AAAA. */
threading::memory_bandwidth_bound_task(spatial_pixels_count * sizeof(float), [&]() {
parallel_for(spatial_size, [&](const int2 texel) {
const Color pixel_color = convolve_input.load_pixel_zero<Color>(texel);
for (const int channel : IndexRange(input_channels_count)) {
float *buffer = image_spatial_domain_channels[channel];
const int64_t index = texel.y * int64_t(spatial_size.x) + texel.x;
buffer[index] = pixel_color[channel];
}
});
});
convolve_input.release();
/* Use doubles to sum the kernel since floats are not stable with threaded summation. We always
* use a double4 even for float kernels for generality, in that case, only the first component
* is initialized. */
threading::EnumerableThreadSpecific<double4> sum_by_thread([]() { return double4(0.0); });
/* Compute the kernel while zero padding to match the spatial size. */
const int2 kernel_center = kernel_size / 2;
parallel_for(spatial_size, [&](const int2 texel) {
/* We offset the computed kernel with wrap around such that it is centered at the zero
* point, which is the expected format for doing circular convolutions in the frequency
* domain. */
const int2 centered_texel = kernel_center - texel;
const int2 wrapped_texel = int2(mod_i(centered_texel.x, spatial_size.x),
mod_i(centered_texel.y, spatial_size.y));
const float4 kernel_value = is_color_kernel ?
float4(convolve_kernel.load_pixel_zero<Color>(wrapped_texel)) :
float4(convolve_kernel.load_pixel_zero<float>(wrapped_texel));
for (const int channel : IndexRange(kernel_channels_count)) {
float *buffer = kernel_spatial_domain_channels[channel];
buffer[texel.x + texel.y * int64_t(spatial_size.x)] = kernel_value[channel];
}
sum_by_thread.local() += double4(kernel_value);
});
convolve_kernel.release();
/* The computed kernel is not normalized and should be normalized, but instead of normalizing the
* kernel during computation, we normalize it in the frequency domain when convolving the kernel
* to the image since we will be doing sample normalization anyways. This is okay since the
* Fourier transform is linear. */
const float4 sum = float4(
std::accumulate(sum_by_thread.begin(), sum_by_thread.end(), double4(0.0)));
const float4 sanitized_sum = float4(sum[0] == 0.0f ? 1.0f : sum[0],
sum[1] == 0.0f ? 1.0f : sum[1],
sum[2] == 0.0f ? 1.0f : sum[2],
sum[3] == 0.0f ? 1.0f : sum[3]);
const float4 normalization_factor = normalize_kernel ? sanitized_sum : float4(1.0f);
/* Transform all necessary data from the real domain to the frequency domain. */
threading::parallel_for(
forward_transform_tasks.index_range(), 1, [&](const IndexRange sub_range) {
for (const int64_t i : sub_range) {
fftwf_execute_dft_r2c(
forward_plan,
forward_transform_tasks[i].input,
reinterpret_cast<fftwf_complex *>(forward_transform_tasks[i].output));
}
});
/* Multiply the kernel and the image in the frequency domain to perform the convolution. The
* FFT is not normalized, meaning the result of the FFT followed by an inverse FFT will result
* in an image that is scaled by a factor of the product of the width and height, so we take
* that into account by dividing by that scale. See Section 4.8.6 Multi-dimensional Transforms
* of the FFTW manual for more information. */
const float4 normalization_scale = float(spatial_size.x) * spatial_size.y * normalization_factor;
threading::parallel_for(IndexRange(frequency_size.y), 1, [&](const IndexRange sub_y_range) {
for (const int64_t channel : IndexRange(input_channels_count)) {
const int kernel_channel = is_color_kernel ? channel : 0;
std::complex<float> *image_buffer = image_frequency_domain_channels[channel];
const std::complex<float> *kernel_buffer = kernel_frequency_domain_channels[kernel_channel];
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(frequency_size.x)) {
const int64_t index = x + y * int64_t(frequency_size.x);
image_buffer[index] *= kernel_buffer[index] / normalization_scale[kernel_channel];
}
}
}
});
/* Transform channels from the frequency domain to the real domain. */
threading::parallel_for(IndexRange(input_channels_count), 1, [&](const IndexRange sub_range) {
for (const int64_t channel : sub_range) {
fftwf_execute_dft_c2r(
backward_plan,
reinterpret_cast<fftwf_complex *>(image_frequency_domain_channels[channel]),
image_spatial_domain_channels[channel]);
}
});
Result output_cpu = context.create_result(input.type());
output_cpu.allocate_texture(input.domain(), true, ResultStorageType::CPU);
/* Copy the result to the output. */
threading::memory_bandwidth_bound_task(input.size_in_bytes(), [&]() {
parallel_for(image_size, [&](const int2 texel) {
float4 color = float4(0.0f);
for (const int channel : IndexRange(input_channels_count)) {
const int64_t index = texel.x + texel.y * int64_t(spatial_size.x);
color[channel] = image_spatial_domain_channels[channel][index];
}
output_cpu.store_pixel(texel, Color(color));
});
});
if (context.use_gpu()) {
Result output_gpu = output_cpu.upload_to_gpu(true);
output.share_data(output_gpu);
output_gpu.release();
}
else {
output.share_data(output_cpu);
}
output_cpu.release();
#else
UNUSED_VARS(kernel, normalize_kernel);
output.allocate_texture(input.domain());
if (context.use_gpu()) {
GPU_texture_copy(output, input);
}
else {
parallel_for(output.domain().data_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<float4>(texel));
});
}
#endif
}
} // namespace blender::compositor

View File

@@ -0,0 +1,309 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_deriche_gaussian_blur.hh"
#include "COM_algorithm_pad.hh"
#include "COM_deriche_gaussian_coefficients.hh"
namespace blender::compositor {
#define FILTER_ORDER 4
/* See sum_causal_and_non_causal_results. */
static void sum_causal_and_non_causal_results_gpu(Context &context,
const Result &causal_input,
const Result &non_causal_input,
Result &output)
{
gpu::Shader *shader = context.get_shader("compositor_deriche_gaussian_blur_sum");
GPU_shader_bind(shader);
causal_input.bind_as_texture(shader, "causal_input_tx");
non_causal_input.bind_as_texture(shader, "non_causal_input_tx");
const Domain domain = causal_input.domain();
const Domain transposed_domain = domain.transposed();
output.allocate_texture(transposed_domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
causal_input.unbind_as_texture();
non_causal_input.unbind_as_texture();
output.unbind_as_image();
}
/* See sum_causal_and_non_causal_results. */
static void sum_causal_and_non_causal_results_cpu(const Result &causal_input,
const Result &non_causal_input,
Result &output)
{
const Domain domain = causal_input.domain();
const Domain transposed_domain = domain.transposed();
output.allocate_texture(transposed_domain);
parallel_for(domain.data_size, [&](const int2 texel) {
/* The Deriche filter is a parallel interconnection filter, meaning its output is the sum of
* its causal and non causal filters. */
float4 filter_output = float4(causal_input.load_pixel<Color>(texel)) +
float4(non_causal_input.load_pixel<Color>(texel));
/* Write the color using the transposed texel. See the sum_causal_and_non_causal_results method
* in the deriche_gaussian_blur.cc file for more information on the rational behind this. */
output.store_pixel(int2(texel.y, texel.x), Color(filter_output));
});
}
/* Sum the causal and non causal outputs of the filter and write the sum to the output. This is
* because the Deriche filter is a parallel interconnection filter, meaning its output is the sum
* of its causal and non causal filters. The output is expected not to be allocated as it will be
* allocated internally.
*
* The output is allocated and written transposed, that is, with a height equivalent to the width
* of the input and vice versa. This is done as a performance optimization. The blur pass will
* blur the image horizontally and write it to the intermediate output transposed. Then the
* vertical pass will execute the same horizontal blur shader, but since its input is transposed,
* it will effectively do a vertical blur and write to the output transposed, effectively undoing
* the transposition in the horizontal pass. This is done to improve spatial cache locality in the
* shader and to avoid having two separate shaders for each blur pass. */
static void sum_causal_and_non_causal_results(Context &context,
const Result &causal_input,
const Result &non_causal_input,
Result &output)
{
if (context.use_gpu()) {
sum_causal_and_non_causal_results_gpu(context, causal_input, non_causal_input, output);
}
else {
sum_causal_and_non_causal_results_cpu(causal_input, non_causal_input, output);
}
}
static void blur_pass_gpu(Context &context,
const Result &input,
Result &causal_result,
Result &non_causal_result,
const float sigma)
{
gpu::Shader *shader = context.get_shader("compositor_deriche_gaussian_blur");
GPU_shader_bind(shader);
const DericheGaussianCoefficients &coefficients =
context.cache_manager().deriche_gaussian_coefficients.get(context, sigma);
GPU_shader_uniform_4fv(shader,
"causal_feedforward_coefficients",
float4(coefficients.causal_feedforward_coefficients()));
GPU_shader_uniform_4fv(shader,
"non_causal_feedforward_coefficients",
float4(coefficients.non_causal_feedforward_coefficients()));
GPU_shader_uniform_4fv(
shader, "feedback_coefficients", float4(coefficients.feedback_coefficients()));
GPU_shader_uniform_1f(
shader, "causal_boundary_coefficient", float(coefficients.causal_boundary_coefficient()));
GPU_shader_uniform_1f(shader,
"non_causal_boundary_coefficient",
float(coefficients.non_causal_boundary_coefficient()));
input.bind_as_texture(shader, "input_tx");
const Domain domain = input.domain();
causal_result.allocate_texture(domain);
non_causal_result.allocate_texture(domain);
causal_result.bind_as_image(shader, "causal_output_img");
non_causal_result.bind_as_image(shader, "non_causal_output_img");
/* The second dispatch dimension is two dispatches, one for the causal filter and one for the non
* causal one. */
compute_dispatch_threads_at_least(shader, int2(domain.data_size.y, 2), int2(128, 2));
GPU_shader_unbind();
input.unbind_as_texture();
causal_result.unbind_as_image();
non_causal_result.unbind_as_image();
}
static void blur_pass_cpu(Context &context,
const Result &input,
Result &causal_output,
Result &non_causal_output,
const float sigma)
{
const DericheGaussianCoefficients &coefficients =
context.cache_manager().deriche_gaussian_coefficients.get(context, sigma);
const float4 causal_feedforward_coefficients = float4(
coefficients.causal_feedforward_coefficients());
const float4 non_causal_feedforward_coefficients = float4(
coefficients.non_causal_feedforward_coefficients());
const float4 feedback_coefficients = float4(coefficients.feedback_coefficients());
const float causal_boundary_coefficient = float(coefficients.causal_boundary_coefficient());
const float non_causal_boundary_coefficient = float(
coefficients.non_causal_boundary_coefficient());
const Domain domain = input.domain();
causal_output.allocate_texture(domain);
non_causal_output.allocate_texture(domain);
/* The first dispatch dimension is two dispatches, one for the causal filter and one for the non
* causal one. */
const int2 parallel_for_size = int2(2, domain.data_size.y);
/* Blur the input horizontally by applying a fourth order IIR filter approximating a Gaussian
* filter using Deriche's design method. This is based on the following paper:
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss. INRIA,
* 1993.
*
* We run two filters per row in parallel, one for the causal filter and one for the non causal
* filter, storing the result of each separately. See the DericheGaussianCoefficients class and
* the implementation for more information. */
parallel_for(parallel_for_size, [&](const int2 invocation) {
/* The code runs parallel across rows but serially across columns. */
int y = invocation.y;
int width = input.domain().data_size.x;
/* The second dispatch dimension is two dispatches, one for the causal filter and one for the
* non causal one. */
bool is_causal = invocation.x == 0;
float4 feedforward_coefficients = is_causal ? causal_feedforward_coefficients :
non_causal_feedforward_coefficients;
float boundary_coefficient = is_causal ? causal_boundary_coefficient :
non_causal_boundary_coefficient;
/* Create an array that holds the last FILTER_ORDER inputs along with the current input. The
* current input is at index 0 and the oldest input is at index FILTER_ORDER. We assume Neumann
* boundary condition, so we initialize all inputs by the boundary pixel. */
int2 boundary_texel = is_causal ? int2(0, y) : int2(width - 1, y);
float4 input_boundary = float4(input.load_pixel<Color>(boundary_texel));
float4 inputs[FILTER_ORDER + 1] = {
input_boundary, input_boundary, input_boundary, input_boundary, input_boundary};
/* Create an array that holds the last FILTER_ORDER outputs along with the current output. The
* current output is at index 0 and the oldest output is at index FILTER_ORDER. We assume
* Neumann boundary condition, so we initialize all outputs by the boundary pixel multiplied by
* the boundary coefficient. See the DericheGaussianCoefficients class for more information on
* the boundary handing. */
float4 output_boundary = input_boundary * boundary_coefficient;
float4 outputs[FILTER_ORDER + 1] = {
output_boundary, output_boundary, output_boundary, output_boundary, output_boundary};
for (int x = 0; x < width; x++) {
/* Run forward across rows for the causal filter and backward for the non causal filter. */
int2 texel = is_causal ? int2(x, y) : int2(width - 1 - x, y);
inputs[0] = float4(input.load_pixel<Color>(texel));
/* Compute Equation (28) for the causal filter or Equation (29) for the non causal filter.
* The only difference is that the non causal filter ignores the current value and starts
* from the previous input, as can be seen in the subscript of the first input term in both
* equations. So add one while indexing the non causal inputs. */
outputs[0] = float4(0.0f);
int first_input_index = is_causal ? 0 : 1;
for (int i = 0; i < FILTER_ORDER; i++) {
outputs[0] += feedforward_coefficients[i] * inputs[first_input_index + i];
outputs[0] -= feedback_coefficients[i] * outputs[i + 1];
}
/* Store the causal and non causal outputs independently, then sum them in a separate shader
* dispatch for better parallelism. */
if (is_causal) {
causal_output.store_pixel(texel, Color(outputs[0]));
}
else {
non_causal_output.store_pixel(texel, Color(outputs[0]));
}
/* Shift the inputs temporally by one. The oldest input is discarded, while the current input
* will retain its value but will be overwritten with the new current value in the next
* iteration. */
for (int i = FILTER_ORDER; i >= 1; i--) {
inputs[i] = inputs[i - 1];
}
/* Shift the outputs temporally by one. The oldest output is discarded, while the current
* output will retain its value but will be overwritten with the new current value in the
* next iteration. */
for (int i = FILTER_ORDER; i >= 1; i--) {
outputs[i] = outputs[i - 1];
}
}
});
}
static void blur_pass(Context &context, const Result &input, Result &output, const float sigma)
{
Result causal_result = context.create_result(ResultType::Color);
Result non_causal_result = context.create_result(ResultType::Color);
if (context.use_gpu()) {
blur_pass_gpu(context, input, causal_result, non_causal_result, sigma);
}
else {
blur_pass_cpu(context, input, causal_result, non_causal_result, sigma);
}
sum_causal_and_non_causal_results(context, causal_result, non_causal_result, output);
causal_result.release();
non_causal_result.release();
}
/* Computes the inverse of compute_sigma_from_radius in recursive_gaussian_blur.cc, see that
* function for more information. */
static float2 compute_radius_from_sigma(const float2 sigma)
{
return sigma * 3.0f;
}
void deriche_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &sigma,
const bool extend_bounds)
{
BLI_assert_msg(math::reduce_max(sigma) >= 3.0f,
"Deriche filter is slower and less accurate than direct convolution for sigma "
"values less 3. Use direct convolution blur instead.");
BLI_assert_msg(math::reduce_max(sigma) < 32.0f,
"Deriche filter is not accurate nor numerically stable for sigma values larger "
"than 32. Use Van Vliet filter instead.");
if (extend_bounds) {
const int2 padding_size = int2(math::ceil(compute_radius_from_sigma(sigma)));
Result padded_input = context.create_result(input.type());
pad(context, input, padded_input, int2(padding_size.x, 0), PaddingMethod::Zero);
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, padded_input, horizontal_pass_result, sigma.x);
padded_input.release();
Result padded_horizontal_pass_result = context.create_result(input.type());
pad(context,
horizontal_pass_result,
padded_horizontal_pass_result,
int2(padding_size.y, 0),
PaddingMethod::Zero);
horizontal_pass_result.release();
blur_pass(context, padded_horizontal_pass_result, output, sigma.y);
padded_horizontal_pass_result.release();
}
else {
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, input, horizontal_pass_result, sigma.x);
blur_pass(context, horizontal_pass_result, output, sigma.y);
horizontal_pass_result.release();
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,56 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_extract_alpha.hh"
namespace blender::compositor {
static void extract_alpha_gpu(Context &context, const Result &input, Result &output)
{
gpu::Shader *shader = context.get_shader("compositor_convert_color_to_alpha");
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
}
static void extract_alpha_cpu(const Result &input, Result &output)
{
output.allocate_texture(input.domain());
parallel_for(input.domain().data_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel<Color>(texel).a);
});
}
void extract_alpha(Context &context, const Result &input, Result &output)
{
if (input.is_single_value()) {
output.allocate_single_value();
output.set_single_value(input.get_single_value<Color>().a);
return;
}
if (context.use_gpu()) {
extract_alpha_gpu(context, input, output);
}
else {
extract_alpha_cpu(input, output);
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,148 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include <utility>
#include "BLI_assert.h"
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_jump_flooding.hh"
namespace blender::compositor {
static void jump_flooding_pass_gpu(Context &context, Result &input, Result &output, int step_size)
{
gpu::Shader *shader = context.get_shader("compositor_jump_flooding", ResultPrecision::Half);
GPU_shader_bind(shader);
GPU_shader_uniform_1i(shader, "step_size", step_size);
input.bind_as_texture(shader, "input_tx");
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
}
/* This function implements a single pass of the Jump Flooding algorithm described in sections 3.1
* and 3.2 of the paper:
*
* Rong, Guodong, and Tiow-Seng Tan. "Jump flooding in GPU with applications to Voronoi diagram
* and distance transform." Proceedings of the 2006 symposium on Interactive 3D graphics and
* games. 2006.
*
* The function is a straightforward implementation of the aforementioned sections of the paper,
* noting that the nil special value in the paper is equivalent to JUMP_FLOODING_NON_FLOODED_VALUE.
*
* The `COM_algorithm_jump_flooding.hh` header contains the necessary utility functions to
* initialize and encode the jump flooding values. */
static void jump_flooding_pass_cpu(Result &input, Result &output, int step_size)
{
parallel_for(input.domain().data_size, [&](const int2 texel) {
/* For each of the previously flooded pixels in the 3x3 window of the given step size around
* the center pixel, find the position of the closest seed pixel that is closest to the current
* center pixel. */
int2 closest_seed_texel = int2(0);
float minimum_squared_distance = std::numeric_limits<float>::max();
for (int j = -1; j <= 1; j++) {
for (int i = -1; i <= 1; i++) {
int2 offset = int2(i, j) * step_size;
/* Use #JUMP_FLOODING_NON_FLOODED_VALUE as a fallback value to exempt out of bound pixels
* from the loop as can be seen in the following continue condition. */
int2 fallback = JUMP_FLOODING_NON_FLOODED_VALUE;
int2 jump_flooding_value = input.load_pixel_fallback(texel + offset, fallback);
/* The pixel is either not flooded yet or is out of bound, so skip it. */
if (jump_flooding_value == JUMP_FLOODING_NON_FLOODED_VALUE) {
continue;
}
/* The neighboring pixel is flooded, so its flooding value is the texel of the closest seed
* pixel to this neighboring pixel. */
int2 closest_seed_texel_to_neighbor = jump_flooding_value;
/* Compute the squared distance to the neighbor's closest seed pixel. */
float squared_distance = math::distance_squared(float2(closest_seed_texel_to_neighbor),
float2(texel));
if (squared_distance < minimum_squared_distance) {
minimum_squared_distance = squared_distance;
closest_seed_texel = closest_seed_texel_to_neighbor;
}
}
}
/* If the minimum squared distance is still #std::numeric_limits<float>::max(), that means the
* loop never got past the continue condition and thus no flooding happened. If flooding
* happened, we encode the closest seed texel in the format expected by the algorithm. */
bool flooding_happened = minimum_squared_distance != std::numeric_limits<float>::max();
int2 jump_flooding_value = encode_jump_flooding_value(closest_seed_texel, flooding_happened);
output.store_pixel(texel, jump_flooding_value);
});
}
static void jump_flooding_pass(Context &context, Result &input, Result &output, int step_size)
{
if (context.use_gpu()) {
jump_flooding_pass_gpu(context, input, output, step_size);
}
else {
jump_flooding_pass_cpu(input, output, step_size);
}
}
void jump_flooding(Context &context, Result &input, Result &output)
{
BLI_assert(input.type() == ResultType::Int2);
BLI_assert(output.type() == ResultType::Int2);
/* First, run a jump flooding pass with a step size of 1. This initial pass is proposed by the
* 1+FJA variant to improve accuracy. */
Result initial_flooded_result = context.create_result(ResultType::Int2, ResultPrecision::Half);
initial_flooded_result.allocate_texture(input.domain());
jump_flooding_pass(context, input, initial_flooded_result, 1);
/* We compute the result using a ping-pong buffer, so create an intermediate result. */
Result *result_to_flood = &initial_flooded_result;
Result intermediate_result = context.create_result(ResultType::Int2, ResultPrecision::Half);
intermediate_result.allocate_texture(input.domain());
Result *result_after_flooding = &intermediate_result;
/* The algorithm starts with a step size that is half the size of the image. However, the
* algorithm assumes a square image that is a power of two in width without loss of generality.
* To generalize that, we use half the next power of two of the maximum dimension. */
const int max_size = math::max(input.domain().data_size.x, input.domain().data_size.y);
int step_size = power_of_2_max_i(max_size) / 2;
/* Successively apply a jump flooding pass, halving the step size every time and swapping the
* ping-pong buffers. */
while (step_size != 0) {
jump_flooding_pass(context, *result_to_flood, *result_after_flooding, step_size);
std::swap(result_to_flood, result_after_flooding);
step_size /= 2;
}
/* Notice that the output of the last pass is stored in result_to_flood due to the last swap, so
* steal the data from it and release the other buffer. */
result_after_flooding->release();
output.share_data(*result_to_flood);
result_to_flood->release();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,104 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_morphological_blur.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
namespace blender::compositor {
static const char *get_shader(MorphologicalBlurOperation operation)
{
switch (operation) {
case MorphologicalBlurOperation::Dilate:
return "compositor_morphological_blur_dilate";
case MorphologicalBlurOperation::Erode:
return "compositor_morphological_blur_erode";
}
BLI_assert_unreachable();
return nullptr;
}
static void apply_morphological_operator_gpu(Context &context,
const Result &input,
Result &blurred_input,
const MorphologicalBlurOperation operation)
{
gpu::Shader *shader = context.get_shader(get_shader(operation));
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
blurred_input.bind_as_image(shader, "blurred_input_img", true);
Domain domain = input.domain();
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input.unbind_as_texture();
blurred_input.unbind_as_image();
}
static void apply_morphological_operator_cpu(const Result &input,
Result &blurred_input,
const MorphologicalBlurOperation operation)
{
Domain domain = input.domain();
switch (operation) {
case MorphologicalBlurOperation::Dilate:
parallel_for(domain.data_size, [&](const int2 texel) {
float input_value = input.load_pixel<float>(texel);
float blurred_value = blurred_input.load_pixel<float>(texel);
blurred_input.store_pixel(texel, math::max(input_value, blurred_value));
});
break;
case MorphologicalBlurOperation::Erode:
parallel_for(domain.data_size, [&](const int2 texel) {
float input_value = input.load_pixel<float>(texel);
float blurred_value = blurred_input.load_pixel<float>(texel);
blurred_input.store_pixel(texel, math::min(input_value, blurred_value));
});
break;
}
}
/* Apply the morphological operator (minimum or maximum) on the input and the blurred input. The
* output is written to the blurred_input in-place. */
static void apply_morphological_operator(Context &context,
const Result &input,
Result &blurred_input,
const MorphologicalBlurOperation operation)
{
if (context.use_gpu()) {
apply_morphological_operator_gpu(context, input, blurred_input, operation);
}
else {
apply_morphological_operator_cpu(input, blurred_input, operation);
}
}
void morphological_blur(Context &context,
const Result &input,
Result &output,
const float2 &radius,
const MorphologicalBlurOperation operation,
const math::FilterKernel filter_type)
{
BLI_assert(input.type() == ResultType::Float);
symmetric_separable_blur(context, input, output, radius, filter_type);
apply_morphological_operator(context, input, output, operation);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,117 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_morphological_distance.hh"
namespace blender::compositor {
static const char *get_shader_name(const int distance)
{
if (distance > 0) {
return "compositor_morphological_distance_dilate";
}
return "compositor_morphological_distance_erode";
}
static void morphological_distance_gpu(Context &context,
const Result &input,
Result &output,
const int distance)
{
gpu::Shader *shader = context.get_shader(get_shader_name(distance));
GPU_shader_bind(shader);
/* Pass the absolute value of the distance. We have specialized shaders for each sign. */
GPU_shader_uniform_1i(shader, "radius", math::abs(distance));
input.bind_as_texture(shader, "input_tx");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
output.unbind_as_image();
input.unbind_as_texture();
}
template<bool IsDilate>
static void morphological_distance_cpu(const Result &input,
Result &output,
const int structuring_element_radius)
{
output.allocate_texture(input.domain());
const float limit = IsDilate ? std::numeric_limits<float>::lowest() :
std::numeric_limits<float>::max();
const auto morphology_operator = [](const float a, const float b) {
if constexpr (IsDilate) {
return math::max(a, b);
}
else {
return math::min(a, b);
}
};
const int2 image_size = input.domain().data_size;
const int radius_squared = math::square(structuring_element_radius);
/* Find the minimum/maximum value in the circular window of the given radius around the pixel.
* By circular window, we mean that pixels in the window whose distance to the center of window
* is larger than the given radius are skipped and not considered. Consequently, the dilation
* or erosion that take place produces round results as opposed to squarish ones. This is
* essentially a morphological operator with a circular structuring element. */
parallel_for(image_size, [&](const int2 texel) {
/* Compute the start and end bounds of the window such that no out-of-bounds processing happen
* in the loops. */
const int2 start = math::max(texel - structuring_element_radius, int2(0)) - texel;
const int2 end = math::min(texel + structuring_element_radius + 1, image_size) - texel;
float value = limit;
for (int y = start.y; y < end.y; y++) {
const int yy = y * y;
for (int x = start.x; x < end.x; x++) {
if (x * x + yy > radius_squared) {
continue;
}
value = morphology_operator(value, input.load_pixel<float>(texel + int2(x, y)));
}
}
output.store_pixel(texel, value);
});
}
void morphological_distance(Context &context,
const Result &input,
Result &output,
const int distance)
{
if (context.use_gpu()) {
morphological_distance_gpu(context, input, output, distance);
}
else {
if (distance > 0) {
morphological_distance_cpu<true>(input, output, math::abs(distance));
}
else {
morphological_distance_cpu<false>(input, output, math::abs(distance));
}
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,309 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_algorithm_morphological_distance_feather.hh"
#include "COM_context.hh"
#include "COM_morphological_distance_feather_weights.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* The Morphological Distance Feather operation is a linear combination between the result of two
* operations. The first operation is a Gaussian blur with a radius equivalent to the dilate/erode
* distance, which is straightforward and implemented as a separable filter similar to the blur
* operation.
*
* The second operation is an approximation of a morphological inverse distance operation evaluated
* at a distance falloff function. The result of a morphological inverse distance operation is a
* narrow band distance field that starts at its maximum value at boundaries where a difference in
* values took place and linearly deceases until it reaches zero in the span of a number of pixels
* equivalent to the erode/dilate distance. Additionally, instead of linearly decreasing, the user
* may choose a different falloff which is evaluated at the computed distance. For dilation, the
* distance field decreases outwards, and for erosion, the distance field decreased inwards.
*
* The reason why the result of a Gaussian blur is mixed in with the distance field is because the
* distance field is merely approximated and not accurately computed, the defects of which is more
* apparent away from boundaries and especially at corners where the distance field should take a
* circular shape. That's why the Gaussian blur is mostly mixed only further from boundaries.
*
* The morphological inverse distance operation is approximated using a separable implementation
* and intertwined with the Gaussian blur implementation as follows. A search window of a radius
* equivalent to the dilate/erode distance is applied on the image to find either the minimum or
* maximum pixel value multiplied by its corresponding falloff value in the window. For dilation,
* we try to find the maximum, and for erosion, we try to find the minimum. The implementation uses
* an inverse function to find the minimum. Additionally, we also save the falloff value where the
* minimum or maximum was found. The found value will be that of the narrow band distance field and
* the saved falloff value will be used as the mixing factor with the Gaussian blur.
*
* To make sense of the aforementioned algorithm, assume we are dilating a binary image by 5 pixels
* whose half has a value of 1 and the other half has a value of zero. Consider the following:
*
* - A pixel of value 1 already has the maximum possible value, so its value will remain unchanged
* regardless of its position.
* - A pixel of value 0 that is right at the boundary of the 1's region will have a maximum value
* of around 0.8 depending on the falloff. That's because the search window intersects the 1's
* region, which when multiplied by the falloff gives the first value of the falloff, which is
* larger than the initially zero value computed at the center of the search window.
* - A pixel of value 0 that is 3 pixels away from the boundary will have a maximum value of around
* 0.4 depending on the falloff. That's because the search window intersects the 1's region,
* which when multiplied by the falloff gives the third value of the falloff, which is larger
* than the initially zero value computed at the center of the search window.
* - Finally, a pixel of value 0 that is 6 pixels away from the boundary will have a maximum value
* of 0, because the search window doesn't intersects the 1's region and only spans zero values.
*
* The previous example demonstrates how the distance field naturally arises, and the same goes for
* the erode case, except the minimum value is computed instead. */
template<bool IsErode>
static void morphological_distance_feather_pass(const Result &input,
const MorphologicalDistanceFeatherWeights &weights,
Result &output)
{
/* Notice that the size is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
const int2 size = int2(output.domain().data_size.y, output.domain().data_size.x);
parallel_for(size, [&](const int2 texel) {
/* A value for accumulating the blur result. */
float accumulated_value = 0.0f;
/* Compute the contribution of the center pixel to the blur result. */
float center_value = input.load_pixel<float>(texel);
if constexpr (IsErode) {
center_value = 1.0f - center_value;
}
accumulated_value += center_value * weights.weights.load_pixel<float>(int2(0));
/* Start with the center value as the maximum/minimum distance and reassign to the true maximum
* or minimum in the search loop below. Additionally, the center falloff is always 1.0, so
* start with that. */
float limit_distance = center_value;
float limit_distance_falloff = 1.0f;
/* Compute the contributions of the pixels to the right and left, noting that the weights and
* falloffs textures only store the weights and falloffs for the positive half, but since the
* they are both symmetric, the same weights and falloffs are used for the negative half and we
* compute both of their contributions. */
for (int i = 1; i < weights.weights.domain().data_size.x; i++) {
float weight = weights.weights.load_pixel<float>(int2(i, 0));
float falloff = weights.falloffs.load_pixel<float>(int2(i, 0));
/* Loop for two iterations, where s takes the value of -1 and 1, which is used as the sign
* needed to evaluated the positive and negative sides as explain above. */
for (int s = -1; s < 2; s += 2) {
/* Compute the contribution of the pixel to the blur result. */
float value = input.load_pixel_extended<float>(texel + int2(s * i, 0));
if constexpr (IsErode) {
value = 1.0f - value;
}
accumulated_value += value * weight;
/* The distance is computed such that its highest value is the pixel value itself, so
* multiply the distance falloff by the pixel value. */
float falloff_distance = value * falloff;
/* Find either the maximum or the minimum for the dilate and erode cases respectively. */
if (falloff_distance > limit_distance) {
limit_distance = falloff_distance;
limit_distance_falloff = falloff;
}
}
}
/* Mix between the limit distance and the blurred accumulated value such that the limit
* distance is used for pixels closer to the boundary and the blurred value is used for pixels
* away from the boundary. */
float value = math::interpolate(accumulated_value, limit_distance, limit_distance_falloff);
if constexpr (IsErode) {
value = 1.0f - value;
}
/* Write the value using the transposed texel. See the horizontal pass function for more
* information on the rational behind this. */
output.store_pixel(int2(texel.y, texel.x), value);
});
}
static const char *get_shader_name(const int distance)
{
if (distance > 0) {
return "compositor_morphological_distance_feather_dilate";
}
return "compositor_morphological_distance_feather_erode";
}
static Result horizontal_pass_gpu(Context &context,
const Result &input,
const int distance,
const int falloff_type)
{
gpu::Shader *shader = context.get_shader(get_shader_name(distance));
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
const MorphologicalDistanceFeatherWeights &weights =
context.cache_manager().morphological_distance_feather_weights.get(
context, falloff_type, math::abs(distance));
weights.weights.bind_as_texture(shader, "weights_tx");
weights.falloffs.bind_as_texture(shader, "falloffs_tx");
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will process the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal pass shader, but since its input is
* transposed, it will effectively do a vertical pass and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each of
* the passes. */
const Domain domain = input.domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result output = context.create_result(ResultType::Float);
output.allocate_texture(transposed_domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input.unbind_as_texture();
weights.weights.unbind_as_texture();
weights.falloffs.unbind_as_texture();
output.unbind_as_image();
return output;
}
static Result horizontal_pass_cpu(Context &context,
const Result &input,
const int distance,
const int falloff_type)
{
const MorphologicalDistanceFeatherWeights &weights =
context.cache_manager().morphological_distance_feather_weights.get(
context, falloff_type, math::abs(distance));
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will process the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal pass shader, but since its input is
* transposed, it will effectively do a vertical pass and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each of
* the passes. */
const Domain domain = input.domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result output = context.create_result(ResultType::Float);
output.allocate_texture(transposed_domain);
if (distance > 0) {
morphological_distance_feather_pass<false>(input, weights, output);
}
else {
morphological_distance_feather_pass<true>(input, weights, output);
}
return output;
}
static Result horizontal_pass(Context &context,
const Result &input,
const int distance,
const int falloff_type)
{
if (context.use_gpu()) {
return horizontal_pass_gpu(context, input, distance, falloff_type);
}
return horizontal_pass_cpu(context, input, distance, falloff_type);
}
static void vertical_pass_gpu(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
Result &output,
const int distance,
const int falloff_type)
{
gpu::Shader *shader = context.get_shader(get_shader_name(distance));
GPU_shader_bind(shader);
horizontal_pass_result.bind_as_texture(shader, "input_tx");
const MorphologicalDistanceFeatherWeights &weights =
context.cache_manager().morphological_distance_feather_weights.get(
context, falloff_type, math::abs(distance));
weights.weights.bind_as_texture(shader, "weights_tx");
weights.falloffs.bind_as_texture(shader, "falloffs_tx");
const Domain domain = original_input.domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
/* Notice that the domain is transposed, see the note on the horizontal pass function for more
* information on the reasoning behind this. */
compute_dispatch_threads_at_least(shader, int2(domain.data_size.y, domain.data_size.x));
GPU_shader_unbind();
horizontal_pass_result.unbind_as_texture();
weights.weights.unbind_as_texture();
weights.falloffs.unbind_as_texture();
output.unbind_as_image();
}
static void vertical_pass_cpu(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
Result &output,
const int distance,
const int falloff_type)
{
const MorphologicalDistanceFeatherWeights &weights =
context.cache_manager().morphological_distance_feather_weights.get(
context, falloff_type, math::abs(distance));
const Domain domain = original_input.domain();
output.allocate_texture(domain);
if (distance > 0) {
morphological_distance_feather_pass<false>(horizontal_pass_result, weights, output);
}
else {
morphological_distance_feather_pass<true>(horizontal_pass_result, weights, output);
}
}
static void vertical_pass(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
Result &output,
const int distance,
const int falloff_type)
{
if (context.use_gpu()) {
vertical_pass_gpu(
context, original_input, horizontal_pass_result, output, distance, falloff_type);
}
else {
vertical_pass_cpu(
context, original_input, horizontal_pass_result, output, distance, falloff_type);
}
}
void morphological_distance_feather(Context &context,
const Result &input,
Result &output,
const int distance,
const int falloff_type)
{
Result horizontal_pass_result = horizontal_pass(context, input, distance, falloff_type);
vertical_pass(context, input, horizontal_pass_result, output, distance, falloff_type);
horizontal_pass_result.release();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,137 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_pad.hh"
namespace blender::compositor {
static const char *get_shader_name(const ResultType type, const PaddingMethod padding_method)
{
switch (padding_method) {
case PaddingMethod::Zero:
switch (type) {
case ResultType::Color:
case ResultType::Float4:
return "compositor_pad_zero_float4";
case ResultType::Float:
return "compositor_pad_zero_float";
default:
break;
}
break;
case PaddingMethod::Extend:
switch (type) {
case ResultType::Float2:
return "compositor_pad_extend_float2";
case ResultType::Float:
return "compositor_pad_extend_float";
default:
break;
}
break;
}
BLI_assert_unreachable();
return "";
}
static void zero_pad_gpu(Context &context,
const Result &input,
Result &output,
const int2 size,
const PaddingMethod padding_method)
{
gpu::Shader *shader = context.get_shader(get_shader_name(input.type(), padding_method));
GPU_shader_bind(shader);
GPU_shader_uniform_2iv(shader, "size", size);
input.bind_as_texture(shader, "input_tx");
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, output.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
output.unbind_as_image();
}
static void zero_pad_cpu(const Result &input,
Result &output,
const int2 size,
const PaddingMethod padding_method)
{
const int2 output_size = output.domain().data_size;
switch (padding_method) {
case PaddingMethod::Zero:
switch (input.type()) {
case ResultType::Color:
parallel_for(output_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel_zero<Color>(texel - size));
});
break;
case ResultType::Float4:
parallel_for(output_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel_zero<float4>(texel - size));
});
break;
case ResultType::Float:
parallel_for(output_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel_zero<float>(texel - size));
});
break;
default:
BLI_assert_unreachable();
}
break;
case PaddingMethod::Extend:
switch (input.type()) {
case ResultType::Float:
parallel_for(output_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel_extended<float>(texel - size));
});
break;
case ResultType::Float2:
parallel_for(output_size, [&](const int2 texel) {
output.store_pixel(texel, input.load_pixel_extended<float2>(texel - size));
});
break;
default:
BLI_assert_unreachable();
}
break;
}
}
void pad(Context &context,
const Result &input,
Result &output,
const int2 size,
const PaddingMethod padding_method)
{
if (input.is_single_value()) {
output.share_data(input);
return;
}
Domain extended_domain = input.domain();
extended_domain.data_size += size * 2;
extended_domain.display_size += size * 2;
output.allocate_texture(extended_domain);
if (context.use_gpu()) {
zero_pad_gpu(context, input, output, size, padding_method);
}
else {
zero_pad_cpu(input, output, size, padding_method);
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,600 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include <limits>
#include "BLI_index_range.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "MEM_guardedalloc.h"
#include "GPU_compute.hh"
#include "GPU_shader.hh"
#include "GPU_state.hh"
#include "GPU_texture.hh"
#include "GPU_texture_pool.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_algorithm_parallel_reduction.hh"
namespace blender::compositor {
/* Reduces the given texture into a single value and returns it. The return value should be freed
* by a call to MEM_delete. The return value is either a pointer to a float, or a pointer to an
* array of floats that represents a vector. This depends on the given format, which should be
* compatible with the reduction shader.
*
* The given reduction shader should be bound when calling the function and the shader is expected
* to be derived from the compositor_parallel_reduction.glsl shader, see that file for more
* information. Also see the compositor_parallel_reduction_info.hh file for example shader
* definitions. */
static float *parallel_reduction_dispatch(gpu::Texture *texture,
gpu::Shader *shader,
gpu::TextureFormat format)
{
GPU_shader_uniform_1b(shader, "is_initial_reduction", true);
gpu::Texture *texture_to_reduce = texture;
int2 size_to_reduce = int2(GPU_texture_width(texture), GPU_texture_height(texture));
/* Dispatch the reduction shader until the texture reduces to a single pixel. */
while (size_to_reduce != int2(1)) {
const int2 reduced_size = math::divide_ceil(size_to_reduce, int2(16));
gpu::Texture *reduced_texture = gpu::TexturePool::get().acquire_texture_2d(
reduced_size, 1, format, GPU_TEXTURE_USAGE_GENERAL);
GPU_memory_barrier(GPU_BARRIER_TEXTURE_FETCH);
const int texture_image_unit = GPU_shader_get_sampler_binding(shader, "input_tx");
GPU_texture_bind(texture_to_reduce, texture_image_unit);
const int image_unit = GPU_shader_get_sampler_binding(shader, "output_img");
GPU_texture_image_bind(reduced_texture, image_unit);
GPU_compute_dispatch(shader, reduced_size.x, reduced_size.y, 1);
GPU_texture_image_unbind(reduced_texture);
GPU_texture_unbind(texture_to_reduce);
/* Release the input texture only if it is not the source texture, since the source texture is
* not acquired or owned by the function. */
if (texture_to_reduce != texture) {
gpu::TexturePool::get().release_texture(texture_to_reduce);
}
texture_to_reduce = reduced_texture;
size_to_reduce = reduced_size;
GPU_shader_uniform_1b(shader, "is_initial_reduction", false);
}
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
float *pixel = static_cast<float *>(GPU_texture_read(texture_to_reduce, GPU_DATA_FLOAT, 0));
/* Release the final texture only if it is not the source texture, since the source texture is
* not acquired or owned by the function. */
if (texture_to_reduce != texture) {
gpu::TexturePool::get().release_texture(texture_to_reduce);
}
return pixel;
}
/* Reduces the given function in parallel over the given 2D range, the reduction function should
* have the given identity value. The given function gets as arguments the texel coordinates of the
* element of the range as well as a reference to the value where the result should be accumulated,
* while the reduction function gets a reference to two values and returns their reduction. */
template<typename Value, typename Function, typename Reduction>
static Value parallel_reduce(const int2 range,
const Value &identity,
const Function &function,
const Reduction &reduction)
{
return threading::parallel_reduce(
IndexRange(range.y),
64,
identity,
[&](const IndexRange sub_y_range, const Value &initial_value) {
Value result = initial_value;
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(range.x)) {
function(int2(x, y), result);
}
}
return result;
},
reduction);
}
/* --------------------------------------------------------------------
* Sum Reductions.
*/
static float4 sum_color_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_sum_color", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Color, ResultPrecision::Full));
const float4 sum = float4(reduced_value);
MEM_delete(reduced_value);
GPU_shader_unbind();
return sum;
}
static float4 sum_color_cpu(const Result &input)
{
return float4(parallel_reduce(
input.domain().data_size,
double4(0.0),
[&](const int2 texel, double4 &accumulated_value) {
accumulated_value += double4(float4(input.load_pixel<Color>(texel)));
},
[&](const double4 &a, const double4 &b) { return a + b; }));
}
float4 sum_color(Context &context, const Result &input)
{
if (context.use_gpu()) {
return sum_color_gpu(context, input);
}
return sum_color_cpu(input);
}
static float sum_log_luminance_gpu(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
gpu::Shader *shader = context.get_shader("compositor_sum_log_luminance", ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float sum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return sum;
}
static float sum_log_luminance_cpu(const Result &input, const float3 &luminance_coefficients)
{
return float(parallel_reduce(
input.domain().data_size,
0.0,
[&](const int2 texel, double &accumulated_value) {
const float luminance = math::dot(float4(input.load_pixel<Color>(texel)).xyz(),
luminance_coefficients);
accumulated_value += std::log(math::max(luminance, 1e-5f));
},
[&](const double &a, const double &b) { return a + b; }));
}
float sum_log_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
if (context.use_gpu()) {
return sum_log_luminance_gpu(context, input, luminance_coefficients);
}
return sum_log_luminance_cpu(input, luminance_coefficients);
}
/* --------------------------------------------------------------------
* Sum Of Squared Difference Reductions.
*/
static float4 sum_squared_difference_color_gpu(Context &context,
const Result &input,
const float4 subtrahend)
{
gpu::Shader *shader = context.get_shader("compositor_sum_squared_difference_color",
ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_4fv(shader, "subtrahend", subtrahend);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Color, ResultPrecision::Full));
const float4 sum = float4(reduced_value);
MEM_delete(reduced_value);
GPU_shader_unbind();
return sum;
}
static float4 sum_squared_difference_color_cpu(const Result &input, const float4 subtrahend)
{
return float4(parallel_reduce(
input.domain().data_size,
double4(0.0),
[&](const int2 texel, double4 &accumulated_value) {
accumulated_value += double4(
math::square(float4(input.load_pixel<Color>(texel)) - subtrahend));
},
[&](const double4 &a, const double4 &b) { return a + b; }));
}
float4 sum_squared_difference_color(Context &context, const Result &input, const float4 subtrahend)
{
BLI_assert(input.type() == ResultType::Color);
if (context.use_gpu()) {
return sum_squared_difference_color_gpu(context, input, subtrahend);
}
return sum_squared_difference_color_cpu(input, subtrahend);
}
/* --------------------------------------------------------------------
* Minimum Reductions.
*/
static float minimum_float_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_minimum_float", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float minimum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return minimum;
}
static float minimum_float_cpu(const Result &input)
{
return float(parallel_reduce(
input.domain().data_size,
std::numeric_limits<float>::max(),
[&](const int2 texel, float &accumulated_value) {
accumulated_value = math::min(accumulated_value, input.load_pixel<float>(texel));
},
[&](const float &a, const float &b) { return math::min(a, b); }));
}
float minimum_float(Context &context, const Result &input)
{
if (context.use_gpu()) {
return minimum_float_gpu(context, input);
}
return minimum_float_cpu(input);
}
static Color minimum_color_cpu(const Result &input)
{
return Color(parallel_reduce(
input.domain().data_size,
float4(std::numeric_limits<float>::max()),
[&](const int2 texel, float4 &accumulated_value) {
accumulated_value = math::min(accumulated_value, float4(input.load_pixel<Color>(texel)));
},
[&](const float4 &a, const float4 &b) { return math::min(a, b); }));
}
static Color minimum_color_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_minimum_float4", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Color, ResultPrecision::Full));
const Color minimum = reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return minimum;
}
Color minimum_color(Context &context, const Result &input)
{
if (context.use_gpu()) {
return minimum_color_gpu(context, input);
}
return minimum_color_cpu(input);
}
static float minimum_luminance_gpu(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
gpu::Shader *shader = context.get_shader("compositor_minimum_luminance", ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float minimum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return minimum;
}
static float minimum_luminance_cpu(const Result &input, const float3 &luminance_coefficients)
{
return float(parallel_reduce(
input.domain().data_size,
std::numeric_limits<float>::max(),
[&](const int2 texel, float &accumulated_value) {
const float luminance = math::dot(float4(input.load_pixel<Color>(texel)).xyz(),
luminance_coefficients);
accumulated_value = math::min(accumulated_value, luminance);
},
[&](const float &a, const float &b) { return math::min(a, b); }));
}
float minimum_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
if (context.use_gpu()) {
return minimum_luminance_gpu(context, input, luminance_coefficients);
}
return minimum_luminance_cpu(input, luminance_coefficients);
}
static float minimum_float_in_range_gpu(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound)
{
gpu::Shader *shader = context.get_shader("compositor_minimum_float_in_range",
ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "lower_bound", lower_bound);
GPU_shader_uniform_1f(shader, "upper_bound", upper_bound);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float minimum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return minimum;
}
static float minimum_float_in_range_cpu(const Result &input,
const float lower_bound,
const float upper_bound)
{
return parallel_reduce(
input.domain().data_size,
upper_bound,
[&](const int2 texel, float &accumulated_value) {
const float value = input.load_pixel<float>(texel);
if ((value <= upper_bound) && (value >= lower_bound)) {
accumulated_value = math::min(accumulated_value, value);
}
},
[&](const float &a, const float &b) { return math::min(a, b); });
}
float minimum_float_in_range(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound)
{
if (context.use_gpu()) {
return minimum_float_in_range_gpu(context, input, lower_bound, upper_bound);
}
return minimum_float_in_range_cpu(input, lower_bound, upper_bound);
}
/* --------------------------------------------------------------------
* Maximum Reductions.
*/
static float maximum_float_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_maximum_float", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float maximum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return maximum;
}
static float maximum_float_cpu(const Result &input)
{
return float(parallel_reduce(
input.domain().data_size,
std::numeric_limits<float>::lowest(),
[&](const int2 texel, float &accumulated_value) {
accumulated_value = math::max(accumulated_value, input.load_pixel<float>(texel));
},
[&](const float &a, const float &b) { return math::max(a, b); }));
}
float maximum_float(Context &context, const Result &input)
{
if (context.use_gpu()) {
return maximum_float_gpu(context, input);
}
return maximum_float_cpu(input);
}
static float2 maximum_float2_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_maximum_float2", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float2, ResultPrecision::Full));
const float2 maximum = reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return maximum;
}
static float2 maximum_float2_cpu(const Result &input)
{
return parallel_reduce(
input.domain().data_size,
float2(std::numeric_limits<float>::lowest()),
[&](const int2 texel, float2 &accumulated_value) {
accumulated_value = math::max(accumulated_value, input.load_pixel<float2>(texel));
},
[&](const float2 &a, const float2 &b) { return math::max(a, b); });
}
float2 maximum_float2(Context &context, const Result &input)
{
if (context.use_gpu()) {
return maximum_float2_gpu(context, input);
}
return maximum_float2_cpu(input);
}
static Color maximum_color_gpu(Context &context, const Result &input)
{
gpu::Shader *shader = context.get_shader("compositor_maximum_float4", ResultPrecision::Full);
GPU_shader_bind(shader);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Color, ResultPrecision::Full));
const Color maximum = Color(reduced_value);
MEM_delete(reduced_value);
GPU_shader_unbind();
return maximum;
}
static Color maximum_color_cpu(const Result &input)
{
return Color(parallel_reduce(
input.domain().data_size,
float4(std::numeric_limits<float>::lowest()),
[&](const int2 texel, float4 &accumulated_value) {
accumulated_value = math::max(accumulated_value, float4(input.load_pixel<Color>(texel)));
},
[&](const float4 &a, const float4 &b) { return math::max(a, b); }));
}
Color maximum_color(Context &context, const Result &input)
{
if (context.use_gpu()) {
return maximum_color_gpu(context, input);
}
return maximum_color_cpu(input);
}
static float maximum_luminance_gpu(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
gpu::Shader *shader = context.get_shader("compositor_maximum_luminance", ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_3fv(shader, "luminance_coefficients", luminance_coefficients);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float maximum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return maximum;
}
static float maximum_luminance_cpu(const Result &input, const float3 &luminance_coefficients)
{
return float(parallel_reduce(
input.domain().data_size,
std::numeric_limits<float>::lowest(),
[&](const int2 texel, float &accumulated_value) {
const float luminance = math::dot(float4(input.load_pixel<Color>(texel)).xyz(),
luminance_coefficients);
accumulated_value = math::max(accumulated_value, luminance);
},
[&](const float &a, const float &b) { return math::max(a, b); }));
}
float maximum_luminance(Context &context,
const Result &input,
const float3 &luminance_coefficients)
{
if (context.use_gpu()) {
return maximum_luminance_gpu(context, input, luminance_coefficients);
}
return maximum_luminance_cpu(input, luminance_coefficients);
}
static float maximum_float_in_range_gpu(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound)
{
gpu::Shader *shader = context.get_shader("compositor_maximum_float_in_range",
ResultPrecision::Full);
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "lower_bound", lower_bound);
GPU_shader_uniform_1f(shader, "upper_bound", upper_bound);
float *reduced_value = parallel_reduction_dispatch(
input, shader, Result::gpu_texture_format(ResultType::Float, ResultPrecision::Full));
const float maximum = *reduced_value;
MEM_delete(reduced_value);
GPU_shader_unbind();
return maximum;
}
static float maximum_float_in_range_cpu(const Result &input,
const float lower_bound,
const float upper_bound)
{
return float(parallel_reduce(
input.domain().data_size,
lower_bound,
[&](const int2 texel, float &accumulated_value) {
const float value = input.load_pixel<float>(texel);
if ((value <= upper_bound) && (value >= lower_bound)) {
accumulated_value = math::max(accumulated_value, value);
}
},
[&](const float &a, const float &b) { return math::max(a, b); }));
}
float maximum_float_in_range(Context &context,
const Result &input,
const float lower_bound,
const float upper_bound)
{
if (context.use_gpu()) {
return maximum_float_in_range_gpu(context, input, lower_bound, upper_bound);
}
return maximum_float_in_range_cpu(input, lower_bound, upper_bound);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,73 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_algorithm_deriche_gaussian_blur.hh"
#include "COM_algorithm_recursive_gaussian_blur.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "COM_algorithm_van_vliet_gaussian_blur.hh"
namespace blender::compositor {
/* Compute the Gaussian sigma from the radius, where the radius is in pixels. Blender's filter is
* truncated at |x| > 3 * sigma as can be seen in the Gauss case of the filter_kernel_value
* function, so we divide by three to get the approximate sigma value. Further, ensure the radius
* is at least 1 since recursive Gaussian implementations can't handle zero radii. */
static float2 compute_sigma_from_radius(float2 radius)
{
return math::max(float2(1.0f), radius) / 3.0f;
}
/* Apply a recursive Gaussian blur algorithm on the input based on the general method outlined
* in the following paper:
*
* Hale, Dave. "Recursive gaussian filters." CWP-546 (2006).
*
* In particular, based on the table in Section 5 Conclusion, for very low radius blur, we use a
* direct separable Gaussian convolution. For medium blur radius, we use the fourth order IIR
* Deriche filter based on the following paper:
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss. INRIA,
* 1993.
*
* For high radius blur, we use the fourth order IIR Van Vliet filter based on the following paper:
*
* Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
* filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
* 98EX170). Vol. 1. IEEE, 1998.
*
* That's because direct convolution is faster and more accurate for very low radius, while the
* Deriche filter is more accurate for medium blur radius, while Van Vliet is more accurate for
* high blur radius. The criteria suggested by the paper is a sigma value threshold of 3 and 32 for
* the Deriche and Van Vliet filters respectively, which we apply on the larger of the two
* dimensions. */
void recursive_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &radius,
const bool extend_bounds)
{
/* The radius is in pixel units, while both recursive implementations expect the sigma value of
* the Gaussian function. */
const float2 sigma = compute_sigma_from_radius(radius);
if (math::reduce_max(sigma) < 3.0f) {
symmetric_separable_blur(
context, input, output, radius, math::FilterKernel::Gauss, extend_bounds);
return;
}
if (math::reduce_max(sigma) < 32.0f) {
deriche_gaussian_blur(context, input, output, sigma, extend_bounds);
return;
}
van_vliet_gaussian_blur(context, input, output, sigma, extend_bounds);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,110 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "MEM_guardedalloc.h"
#include "GPU_compute.hh"
#include "GPU_shader.hh"
#include "GPU_state.hh"
#include "GPU_texture.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_algorithm_sample_pixel.hh"
namespace blender::compositor {
static char const *get_pixel_sampler_shader_name(const Interpolation &interpolation)
{
switch (interpolation) {
case Interpolation::Anisotropic:
case Interpolation::Bicubic:
return "compositor_sample_pixel_bicubic";
case Interpolation::Bilinear:
case Interpolation::Nearest:
return "compositor_sample_pixel";
}
BLI_assert_unreachable();
return "compositor_sample_pixel";
}
static Color sample_pixel_gpu(Context &context,
const Result &input,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
const float2 coordinates)
{
gpu::Shader *shader = context.get_shader(get_pixel_sampler_shader_name(interpolation));
GPU_shader_bind(shader);
GPU_shader_uniform_2fv(shader, "coordinates_u", coordinates);
Result output = context.create_result(input.type());
output.allocate_texture(int2(1));
if (interpolation == Interpolation::Anisotropic) {
GPU_texture_anisotropic_filter(input, true);
GPU_texture_mipmap_mode(input, true, true);
}
else {
const bool use_bilinear = ELEM(interpolation, Interpolation::Bilinear, Interpolation::Bicubic);
GPU_texture_filter_mode(input, use_bilinear);
}
GPU_texture_extend_mode_x(input, map_extension_mode_to_extend_mode(extension_mode_x));
GPU_texture_extend_mode_y(input, map_extension_mode_to_extend_mode(extension_mode_y));
input.bind_as_texture(shader, "input_tx");
output.bind_as_image(shader, "output_img");
GPU_compute_dispatch(shader, 1, 1, 1);
input.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
float *pixel = static_cast<float *>(GPU_texture_read(output, GPU_DATA_FLOAT, 0));
output.release();
Color sampled_color = Color(pixel);
MEM_delete(pixel);
return sampled_color;
}
static Color sample_pixel_cpu(const Result &input,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
const float2 coordinates)
{
return input.sample<Color>(coordinates, interpolation, extension_mode_x, extension_mode_y);
}
/* Samples a pixel from a result. */
Color sample_pixel(Context &context,
const Result &input,
const Interpolation &interpolation,
const Extension &extension_mode_x,
const Extension &extension_mode_y,
const float2 coordinates)
{
BLI_assert(input.type() == ResultType::Color);
if (input.is_single_value()) {
return input.get_single_value<Color>();
}
if (context.use_gpu()) {
return sample_pixel_gpu(
context, input, interpolation, extension_mode_x, extension_mode_y, coordinates);
}
return sample_pixel_cpu(input, interpolation, extension_mode_x, extension_mode_y, coordinates);
}
} // namespace blender::compositor

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,279 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_index_range.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "GPU_compute.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_algorithm_summed_area_table.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Summed Area Table
*
* An implementation of the summed area table algorithm from the paper:
*
* Nehab, Diego, et al. "GPU-efficient recursive filtering and summed-area tables."
*
* This file is a straightforward implementation of each of the four passes described in
* Algorithm SAT in section 6 of the paper. Note that we use Blender's convention of first
* quadrant images, so we call prologues horizontal or X prologues, and we call transposed
* prologues vertical or Y prologues. See each of the functions for more details. */
static const char *get_compute_incomplete_prologues_shader(SummedAreaTableOperation operation)
{
switch (operation) {
case SummedAreaTableOperation::Identity:
return "compositor_summed_area_table_compute_incomplete_prologues_identity";
case SummedAreaTableOperation::Square:
return "compositor_summed_area_table_compute_incomplete_prologues_square";
}
BLI_assert_unreachable();
return "";
}
/* Computes the horizontal and vertical incomplete prologues from the given input using equations
* (42) and (43) to implement the first pass of Algorithm SAT. Those equations accumulatively sum
* each row in each block, writing the final sum to the X incomplete block, then sum each column in
* the X accumulatively summed block, writing the final sum to the Y incomplete block. The output
* is the prologues along the horizontal and vertical directions, where the accumulation axis is
* stored along the vertical axis, so the X prologues are stored transposed for better cache
* locality. */
static void compute_incomplete_prologues(Context &context,
Result &input,
SummedAreaTableOperation operation,
Result &incomplete_x_prologues,
Result &incomplete_y_prologues)
{
gpu::Shader *shader = context.get_shader(get_compute_incomplete_prologues_shader(operation),
ResultPrecision::Full);
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
const int2 group_size = int2(16);
const int2 input_size = input.domain().data_size;
const int2 number_of_groups = math::divide_ceil(input_size, group_size);
incomplete_x_prologues.allocate_texture(Domain(int2(input_size.y, number_of_groups.x)));
incomplete_x_prologues.bind_as_image(shader, "incomplete_x_prologues_img");
incomplete_y_prologues.allocate_texture(Domain(int2(input_size.x, number_of_groups.y)));
incomplete_y_prologues.bind_as_image(shader, "incomplete_y_prologues_img");
GPU_compute_dispatch(shader, number_of_groups.x, number_of_groups.y, 1);
GPU_shader_unbind();
input.unbind_as_texture();
incomplete_x_prologues.unbind_as_image();
incomplete_y_prologues.unbind_as_image();
}
/* Computes the complete X prologues and their sum from the incomplete X prologues using equation
* (44) to implement the second pass of Algorithm SAT. That equation simply sum the incomplete
* prologue and all incomplete prologues before it, writing the sum to the complete prologue. Then,
* each of the complete prologues is summed using parallel reduction writing the sum to the output
* sum for each block. The shader runs in parallel vertically, but serially horizontally. Note that
* the input incomplete X prologues and output complete X prologues are stored transposed for
* better cache locality, but the output sum is stored straight, not transposed. */
static void compute_complete_x_prologues(Context &context,
Result &input,
Result &incomplete_x_prologues,
Result &complete_x_prologues,
Result &complete_x_prologues_sum)
{
gpu::Shader *shader = context.get_shader(
"compositor_summed_area_table_compute_complete_x_prologues", ResultPrecision::Full);
GPU_shader_bind(shader);
incomplete_x_prologues.bind_as_texture(shader, "incomplete_x_prologues_tx");
const int2 group_size = int2(16);
const int2 input_size = input.domain().data_size;
const int2 number_of_groups = math::divide_ceil(input_size, group_size);
complete_x_prologues.allocate_texture(incomplete_x_prologues.domain());
complete_x_prologues.bind_as_image(shader, "complete_x_prologues_img");
complete_x_prologues_sum.allocate_texture(Domain(number_of_groups));
complete_x_prologues_sum.bind_as_image(shader, "complete_x_prologues_sum_img");
GPU_compute_dispatch(shader, number_of_groups.y, 1, 1);
GPU_shader_unbind();
incomplete_x_prologues.unbind_as_texture();
complete_x_prologues.unbind_as_image();
complete_x_prologues_sum.unbind_as_image();
}
/* Computes the complete Y prologues from the incomplete Y prologues using equation (45) to
* implement the third pass of Algorithm SAT. That equation simply sum the incomplete prologue and
* all incomplete prologues before it, then adds the sum of the complete X prologue for the same
* block, writing the sum to the complete prologue. The shader runs in parallel horizontally, but
* serially vertically. */
static void compute_complete_y_prologues(Context &context,
Result &input,
Result &incomplete_y_prologues,
Result &complete_x_prologues_sum,
Result &complete_y_prologues)
{
gpu::Shader *shader = context.get_shader(
"compositor_summed_area_table_compute_complete_y_prologues", ResultPrecision::Full);
GPU_shader_bind(shader);
incomplete_y_prologues.bind_as_texture(shader, "incomplete_y_prologues_tx");
complete_x_prologues_sum.bind_as_texture(shader, "complete_x_prologues_sum_tx");
const int2 group_size = int2(16);
const int2 input_size = input.domain().data_size;
const int2 number_of_groups = math::divide_ceil(input_size, group_size);
complete_y_prologues.allocate_texture(incomplete_y_prologues.domain());
complete_y_prologues.bind_as_image(shader, "complete_y_prologues_img");
GPU_compute_dispatch(shader, number_of_groups.x, 1, 1);
GPU_shader_unbind();
incomplete_y_prologues.unbind_as_texture();
complete_x_prologues_sum.unbind_as_texture();
complete_y_prologues.unbind_as_image();
}
static const char *get_compute_complete_blocks_shader(SummedAreaTableOperation operation)
{
switch (operation) {
case SummedAreaTableOperation::Identity:
return "compositor_summed_area_table_compute_complete_blocks_identity";
case SummedAreaTableOperation::Square:
return "compositor_summed_area_table_compute_complete_blocks_square";
}
BLI_assert_unreachable();
return "";
}
/* Computes the final summed area table blocks from the complete X and Y prologues using equation
* (41) to implement the fourth pass of Algorithm SAT. That equation simply uses an intermediate
* shared memory to cascade the accumulation of rows and then column in each block using the
* prologues as initial values and writes each step of the latter accumulation to the output. */
static void compute_complete_blocks(Context &context,
Result &input,
Result &complete_x_prologues,
Result &complete_y_prologues,
SummedAreaTableOperation operation,
Result &output)
{
gpu::Shader *shader = context.get_shader(get_compute_complete_blocks_shader(operation),
ResultPrecision::Full);
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
complete_x_prologues.bind_as_texture(shader, "complete_x_prologues_tx");
complete_y_prologues.bind_as_texture(shader, "complete_y_prologues_tx");
output.allocate_texture(input.domain());
output.bind_as_image(shader, "output_img", true);
const int2 group_size = int2(16);
const int2 input_size = input.domain().data_size;
const int2 number_of_groups = math::divide_ceil(input_size, group_size);
GPU_compute_dispatch(shader, number_of_groups.x, number_of_groups.y, 1);
GPU_shader_unbind();
input.unbind_as_texture();
complete_x_prologues.unbind_as_texture();
complete_y_prologues.unbind_as_texture();
output.unbind_as_image();
}
static void summed_area_table_gpu(Context &context,
Result &input,
Result &output,
SummedAreaTableOperation operation)
{
Result incomplete_x_prologues = context.create_result(ResultType::Color, ResultPrecision::Full);
Result incomplete_y_prologues = context.create_result(ResultType::Color, ResultPrecision::Full);
compute_incomplete_prologues(
context, input, operation, incomplete_x_prologues, incomplete_y_prologues);
Result complete_x_prologues = context.create_result(ResultType::Color, ResultPrecision::Full);
Result complete_x_prologues_sum = context.create_result(ResultType::Color,
ResultPrecision::Full);
compute_complete_x_prologues(
context, input, incomplete_x_prologues, complete_x_prologues, complete_x_prologues_sum);
incomplete_x_prologues.release();
Result complete_y_prologues = context.create_result(ResultType::Color, ResultPrecision::Full);
compute_complete_y_prologues(
context, input, incomplete_y_prologues, complete_x_prologues_sum, complete_y_prologues);
incomplete_y_prologues.release();
complete_x_prologues_sum.release();
compute_complete_blocks(
context, input, complete_x_prologues, complete_y_prologues, operation, output);
complete_x_prologues.release();
complete_y_prologues.release();
}
/* Computes the summed area table as a cascade of a horizontal summing pass followed by a vertical
* summing pass. */
static void summed_area_table_cpu(Result &input,
Result &output,
SummedAreaTableOperation operation)
{
output.allocate_texture(input.domain());
/* Horizontal summing pass. */
const int2 size = input.domain().data_size;
threading::parallel_for(IndexRange(size.y), 1, [&](const IndexRange range_y) {
for (const int y : range_y) {
float4 accumulated_color = float4(0.0f);
for (const int x : IndexRange(size.x)) {
const int2 texel = int2(x, y);
const float4 color = float4(input.load_pixel<Color>(texel));
accumulated_color += operation == SummedAreaTableOperation::Square ? color * color : color;
output.store_pixel(texel, Color(accumulated_color));
}
}
});
/* Vertical summing pass. */
threading::parallel_for(IndexRange(size.x), 1, [&](const IndexRange range_x) {
for (const int x : range_x) {
float4 accumulated_color = float4(0.0f);
for (const int y : IndexRange(size.y)) {
const int2 texel = int2(x, y);
const float4 color = float4(output.load_pixel<Color>(texel));
accumulated_color += color;
output.store_pixel(texel, Color(accumulated_color));
}
}
});
}
void summed_area_table(Context &context,
Result &input,
Result &output,
SummedAreaTableOperation operation)
{
if (context.use_gpu()) {
summed_area_table_gpu(context, input, output, operation);
}
else {
summed_area_table_cpu(input, output, operation);
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,187 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <type_traits>
#include "BLI_assert.h"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_pad.hh"
#include "COM_algorithm_symmetric_separable_blur.hh"
#include "COM_symmetric_separable_blur_weights.hh"
namespace blender::compositor {
template<typename T>
static void blur_pass(const Result &input, const Result &weights, Result &output)
{
/* Notice that the size is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
const int2 size = int2(output.domain().data_size.y, output.domain().data_size.x);
parallel_for(size, [&](const int2 texel) {
/* Use float4 for Color types since Color does not support arithmetic. */
using AccumulateT = std::conditional_t<std::is_same_v<T, Color>, float4, T>;
AccumulateT accumulated_value = AccumulateT(0);
/* First, compute the contribution of the center pixel. */
AccumulateT center_value = AccumulateT(input.load_pixel_extended<T>(texel));
accumulated_value += center_value * weights.load_pixel<float>(int2(0));
/* Then, compute the contributions of the pixel to the right and left, noting that the
* weights texture only stores the weights for the positive half, but since the filter is
* symmetric, the same weight is used for the negative half and we add both of their
* contributions. */
for (int i = 1; i < weights.domain().data_size.x; i++) {
float weight = weights.load_pixel<float>(int2(i, 0));
accumulated_value += AccumulateT(input.load_pixel_extended<T>(texel + int2(i, 0))) * weight;
accumulated_value += AccumulateT(input.load_pixel_extended<T>(texel + int2(-i, 0))) * weight;
}
/* Write the color using the transposed texel. See the horizontal_pass method for more
* information on the rational behind this. */
output.store_pixel(int2(texel.y, texel.x), T(accumulated_value));
});
}
static const char *get_blur_shader(const ResultType type)
{
switch (type) {
case ResultType::Float:
return "compositor_symmetric_separable_blur_float";
case ResultType::Float4:
case ResultType::Color:
return "compositor_symmetric_separable_blur_float4";
default:
break;
}
BLI_assert_unreachable();
return nullptr;
}
static void blur_pass_gpu(Context &context,
const Result &input,
Result &output,
const float radius,
const math::FilterKernel filter_type)
{
gpu::Shader *shader = context.get_shader(get_blur_shader(input.type()));
GPU_shader_bind(shader);
input.bind_as_texture(shader, "input_tx");
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, radius);
weights.bind_as_texture(shader, "weights_tx");
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will blur the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal blur shader, but since its input is
* transposed, it will effectively do a vertical blur and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each blur
* pass. */
output.allocate_texture(input.domain().transposed());
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
GPU_shader_unbind();
input.unbind_as_texture();
weights.unbind_as_texture();
output.unbind_as_image();
}
static void blur_pass_cpu(Context &context,
const Result &input,
Result &output,
const float radius,
const math::FilterKernel filter_type)
{
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, radius);
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The code will
* blur the image horizontally and write it to the intermediate output transposed. Then the
* vertical pass will execute the same horizontal blur shader, but since its input is transposed,
* it will effectively do a vertical blur and write to the output transposed, effectively undoing
* the transposition in the horizontal pass. This is done to improve spatial cache locality in
* the shader and to avoid having two separate shaders for each blur pass. */
output.allocate_texture(input.domain().transposed());
switch (input.type()) {
case ResultType::Float:
blur_pass<float>(input, weights, output);
break;
case ResultType::Float4:
blur_pass<float4>(input, weights, output);
break;
case ResultType::Color:
blur_pass<Color>(input, weights, output);
break;
default:
BLI_assert_unreachable();
break;
}
}
static void blur_pass(Context &context,
const Result &input,
Result &output,
const float radius,
const math::FilterKernel filter_type)
{
if (context.use_gpu()) {
blur_pass_gpu(context, input, output, radius, filter_type);
return;
}
blur_pass_cpu(context, input, output, radius, filter_type);
}
void symmetric_separable_blur(Context &context,
const Result &input,
Result &output,
const float2 &radius,
const math::FilterKernel filter_type,
const bool extend_bounds)
{
if (extend_bounds) {
const int2 padding_size = int2(math::ceil(radius));
Result padded_input = context.create_result(input.type());
pad(context, input, padded_input, int2(padding_size.x, 0), PaddingMethod::Zero);
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, padded_input, horizontal_pass_result, radius.x, filter_type);
padded_input.release();
Result padded_horizontal_pass_result = context.create_result(input.type());
pad(context,
horizontal_pass_result,
padded_horizontal_pass_result,
int2(padding_size.y, 0),
PaddingMethod::Zero);
horizontal_pass_result.release();
blur_pass(context, padded_horizontal_pass_result, output, radius.y, filter_type);
padded_horizontal_pass_result.release();
}
else {
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, input, horizontal_pass_result, radius.x, filter_type);
blur_pass(context, horizontal_pass_result, output, radius.y, filter_type);
horizontal_pass_result.release();
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,251 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_symmetric_separable_blur_variable_size.hh"
#include "COM_symmetric_separable_blur_weights.hh"
namespace blender::compositor {
static void blur_pass(const Result &input,
const Result &radius_input,
const Result &weights,
Result &output,
const bool is_vertical_pass)
{
/* Notice that the size is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
const int2 size = int2(output.domain().data_size.y, output.domain().data_size.x);
parallel_for(size, [&](const int2 texel) {
float accumulated_weight = 0.0f;
float4 accumulated_color = float4(0.0f);
/* First, compute the contribution of the center pixel. */
float4 center_color = float4(input.load_pixel<Color>(texel));
float center_weight = weights.load_pixel<float>(int2(0));
accumulated_color += center_color * center_weight;
accumulated_weight += center_weight;
/* The dispatch domain is transposed in the vertical pass, so make sure to reverse transpose
* the texel coordinates when loading the radius. See the horizontal_pass function for more
* information. */
int radius = int(
radius_input.load_pixel<float>(is_vertical_pass ? int2(texel.y, texel.x) : texel));
/* Then, compute the contributions of the pixel to the right and left, noting that the
* weights texture only stores the weights for the positive half, but since the filter is
* symmetric, the same weight is used for the negative half and we add both of their
* contributions. */
for (int i = 1; i <= radius; i++) {
/* Add 0.5 to evaluate at the center of the pixels. */
float weight = weights.sample_bilinear_extended<float>(
float2((float(i) + 0.5f) / float(radius + 1), 0.0f));
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(i, 0))) * weight;
accumulated_color += float4(input.load_pixel_extended<Color>(texel + int2(-i, 0))) * weight;
accumulated_weight += weight * 2.0f;
}
/* Write the color using the transposed texel. See the horizontal_pass_cpu function for more
* information on the rational behind this. */
output.store_pixel(int2(texel.y, texel.x), Color(accumulated_color / accumulated_weight));
});
}
static Result horizontal_pass_gpu(Context &context,
const Result &input,
const Result &radius,
const int weights_resolution,
const math::FilterKernel filter_type)
{
gpu::Shader *shader = context.get_shader("compositor_symmetric_separable_blur_variable_size");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "is_vertical_pass", false);
input.bind_as_texture(shader, "input_tx");
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
GPU_texture_filter_mode(weights, true);
GPU_texture_extend_mode(weights, GPU_SAMPLER_EXTEND_MODE_EXTEND);
weights.bind_as_texture(shader, "weights_tx");
radius.bind_as_texture(shader, "radius_tx");
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will blur the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal blur shader, but since its input is
* transposed, it will effectively do a vertical blur and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each blur
* pass. */
Domain domain = input.domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result output = context.create_result(input.type());
output.allocate_texture(transposed_domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
input.unbind_as_texture();
weights.unbind_as_texture();
radius.unbind_as_texture();
output.unbind_as_image();
return output;
}
static Result horizontal_pass_cpu(Context &context,
const Result &input,
const Result &radius,
const int weights_resolution,
const math::FilterKernel filter_type)
{
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
/* We allocate an output image of a transposed size, that is, with a height equivalent to the
* width of the input and vice versa. This is done as a performance optimization. The shader
* will blur the image horizontally and write it to the intermediate output transposed. Then
* the vertical pass will execute the same horizontal blur shader, but since its input is
* transposed, it will effectively do a vertical blur and write to the output transposed,
* effectively undoing the transposition in the horizontal pass. This is done to improve
* spatial cache locality in the shader and to avoid having two separate shaders for each blur
* pass. */
Domain domain = input.domain();
const int2 transposed_domain = int2(domain.data_size.y, domain.data_size.x);
Result output = context.create_result(input.type());
output.allocate_texture(transposed_domain);
blur_pass(input, radius, weights, output, false);
return output;
}
static Result horizontal_pass(Context &context,
const Result &input,
const Result &radius,
const int weights_resolution,
const math::FilterKernel filter_type)
{
if (context.use_gpu()) {
return horizontal_pass_gpu(context, input, radius, weights_resolution, filter_type);
}
return horizontal_pass_cpu(context, input, radius, weights_resolution, filter_type);
}
static void vertical_pass_gpu(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
const Result &radius,
Result &output,
const int weights_resolution,
const math::FilterKernel filter_type)
{
gpu::Shader *shader = context.get_shader("compositor_symmetric_separable_blur_variable_size");
GPU_shader_bind(shader);
GPU_shader_uniform_1b(shader, "is_vertical_pass", true);
horizontal_pass_result.bind_as_texture(shader, "input_tx");
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
GPU_texture_filter_mode(weights, true);
GPU_texture_extend_mode(weights, GPU_SAMPLER_EXTEND_MODE_EXTEND);
weights.bind_as_texture(shader, "weights_tx");
radius.bind_as_texture(shader, "radius_tx");
Domain domain = original_input.domain();
output.allocate_texture(domain);
output.bind_as_image(shader, "output_img");
/* Notice that the domain is transposed, see the note on the horizontal pass method for more
* information on the reasoning behind this. */
compute_dispatch_threads_at_least(shader, int2(domain.data_size.y, domain.data_size.x));
GPU_shader_unbind();
horizontal_pass_result.unbind_as_texture();
output.unbind_as_image();
weights.unbind_as_texture();
radius.unbind_as_texture();
}
static void vertical_pass_cpu(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
const Result &radius,
Result &output,
const int weights_resolution,
const math::FilterKernel filter_type)
{
const Result &weights = context.cache_manager().symmetric_separable_blur_weights.get(
context, filter_type, weights_resolution);
Domain domain = original_input.domain();
output.allocate_texture(domain);
blur_pass(horizontal_pass_result, radius, weights, output, true);
}
static void vertical_pass(Context &context,
const Result &original_input,
const Result &horizontal_pass_result,
const Result &radius,
Result &output,
const int weights_resolution,
const math::FilterKernel filter_type)
{
if (context.use_gpu()) {
vertical_pass_gpu(context,
original_input,
horizontal_pass_result,
radius,
output,
weights_resolution,
filter_type);
}
else {
vertical_pass_cpu(context,
original_input,
horizontal_pass_result,
radius,
output,
weights_resolution,
filter_type);
}
}
void symmetric_separable_blur_variable_size(Context &context,
const Result &input,
const Result &radius,
Result &output,
const int weights_resolution,
const math::FilterKernel filter_type)
{
BLI_assert(input.type() == ResultType::Color);
Result horizontal_pass_result = horizontal_pass(
context, input, radius, weights_resolution, filter_type);
vertical_pass(
context, input, horizontal_pass_result, radius, output, weights_resolution, filter_type);
horizontal_pass_result.release();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,425 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_algorithm_pad.hh"
#include "COM_algorithm_van_vliet_gaussian_blur.hh"
#include "COM_van_vliet_gaussian_coefficients.hh"
namespace blender::compositor {
#define FILTER_ORDER 2
static void sum_causal_and_non_causal_results_gpu(Context &context,
const Result &first_causal_input,
const Result &first_non_causal_input,
const Result &second_causal_input,
const Result &second_non_causal_input,
Result &output)
{
gpu::Shader *shader = context.get_shader("compositor_van_vliet_gaussian_blur_sum");
GPU_shader_bind(shader);
first_causal_input.bind_as_texture(shader, "first_causal_input_tx");
first_non_causal_input.bind_as_texture(shader, "first_non_causal_input_tx");
second_causal_input.bind_as_texture(shader, "second_causal_input_tx");
second_non_causal_input.bind_as_texture(shader, "second_non_causal_input_tx");
const Domain domain = first_causal_input.domain();
const Domain transposed_domain = domain.transposed();
output.allocate_texture(transposed_domain);
output.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
GPU_shader_unbind();
first_causal_input.unbind_as_texture();
first_non_causal_input.unbind_as_texture();
second_causal_input.unbind_as_texture();
second_non_causal_input.unbind_as_texture();
output.unbind_as_image();
}
/* See sum_causal_and_non_causal_results. */
static void sum_causal_and_non_causal_results_cpu(const Result &first_causal_input,
const Result &first_non_causal_input,
const Result &second_causal_input,
const Result &second_non_causal_input,
Result &output)
{
const Domain domain = first_causal_input.domain();
const Domain transposed_domain = domain.transposed();
output.allocate_texture(transposed_domain);
parallel_for(domain.data_size, [&](const int2 texel) {
/* The Van Vliet filter is a parallel interconnection filter, meaning its output is the sum of
* all of its causal and non causal filters. */
float4 filter_output = float4(first_causal_input.load_pixel<Color>(texel)) +
float4(first_non_causal_input.load_pixel<Color>(texel)) +
float4(second_causal_input.load_pixel<Color>(texel)) +
float4(second_non_causal_input.load_pixel<Color>(texel));
/* Write the color using the transposed texel. See the sum_causal_and_non_causal_results method
* for more information on the rational behind this. */
output.store_pixel(int2(texel.y, texel.x), Color(filter_output));
});
}
/* Sum all four of the causal and non causal outputs of the first and second filters and write the
* sum to the output. This is because the Van Vliet filter is implemented as a bank of 2 parallel
* second order filters, meaning its output is the sum of the causal and non causal filters of both
* filters. The output is expected not to be allocated as it will be allocated internally.
*
* The output is allocated and written transposed, that is, with a height equivalent to the width
* of the input and vice versa. This is done as a performance optimization. The blur pass will
* blur the image horizontally and write it to the intermediate output transposed. Then the
* vertical pass will execute the same horizontal blur shader, but since its input is transposed,
* it will effectively do a vertical blur and write to the output transposed, effectively undoing
* the transposition in the horizontal pass. This is done to improve spatial cache locality in the
* shader and to avoid having two separate shaders for each blur pass. */
static void sum_causal_and_non_causal_results(Context &context,
const Result &first_causal_input,
const Result &first_non_causal_input,
const Result &second_causal_input,
const Result &second_non_causal_input,
Result &output)
{
if (context.use_gpu()) {
sum_causal_and_non_causal_results_gpu(context,
first_causal_input,
first_non_causal_input,
second_causal_input,
second_non_causal_input,
output);
}
else {
sum_causal_and_non_causal_results_cpu(first_causal_input,
first_non_causal_input,
second_causal_input,
second_non_causal_input,
output);
}
}
static void blur_pass_gpu(Context &context,
const Result &input,
Result &first_causal_result,
Result &first_non_causal_result,
Result &second_causal_result,
Result &second_non_causal_result,
const float sigma)
{
gpu::Shader *shader = context.get_shader("compositor_van_vliet_gaussian_blur");
GPU_shader_bind(shader);
const VanVlietGaussianCoefficients &coefficients =
context.cache_manager().van_vliet_gaussian_coefficients.get(context, sigma);
GPU_shader_uniform_2fv(
shader, "first_feedback_coefficients", float2(coefficients.first_feedback_coefficients()));
GPU_shader_uniform_2fv(shader,
"first_causal_feedforward_coefficients",
float2(coefficients.first_causal_feedforward_coefficients()));
GPU_shader_uniform_2fv(shader,
"first_non_causal_feedforward_coefficients",
float2(coefficients.first_non_causal_feedforward_coefficients()));
GPU_shader_uniform_2fv(
shader, "second_feedback_coefficients", float2(coefficients.second_feedback_coefficients()));
GPU_shader_uniform_2fv(shader,
"second_causal_feedforward_coefficients",
float2(coefficients.second_causal_feedforward_coefficients()));
GPU_shader_uniform_2fv(shader,
"second_non_causal_feedforward_coefficients",
float2(coefficients.second_non_causal_feedforward_coefficients()));
GPU_shader_uniform_1f(shader,
"first_causal_boundary_coefficient",
float(coefficients.first_causal_boundary_coefficient()));
GPU_shader_uniform_1f(shader,
"first_non_causal_boundary_coefficient",
float(coefficients.first_non_causal_boundary_coefficient()));
GPU_shader_uniform_1f(shader,
"second_causal_boundary_coefficient",
float(coefficients.second_causal_boundary_coefficient()));
GPU_shader_uniform_1f(shader,
"second_non_causal_boundary_coefficient",
float(coefficients.second_non_causal_boundary_coefficient()));
input.bind_as_texture(shader, "input_tx");
const Domain domain = input.domain();
first_causal_result.allocate_texture(domain);
first_causal_result.bind_as_image(shader, "first_causal_output_img");
first_non_causal_result.allocate_texture(domain);
first_non_causal_result.bind_as_image(shader, "first_non_causal_output_img");
second_causal_result.allocate_texture(domain);
second_causal_result.bind_as_image(shader, "second_causal_output_img");
second_non_causal_result.allocate_texture(domain);
second_non_causal_result.bind_as_image(shader, "second_non_causal_output_img");
/* The second dispatch dimension is 4 dispatches, one for the first causal filter, one for the
* first non causal filter, one for the second causal filter, and one for the second non causal
* filter. */
compute_dispatch_threads_at_least(shader, int2(domain.data_size.y, 4), int2(64, 4));
GPU_shader_unbind();
input.unbind_as_texture();
first_causal_result.unbind_as_image();
first_non_causal_result.unbind_as_image();
second_causal_result.unbind_as_image();
second_non_causal_result.unbind_as_image();
}
static void blur_pass_cpu(Context &context,
const Result &input,
Result &first_causal_output,
Result &first_non_causal_output,
Result &second_causal_output,
Result &second_non_causal_output,
const float sigma)
{
const VanVlietGaussianCoefficients &coefficients =
context.cache_manager().van_vliet_gaussian_coefficients.get(context, sigma);
const float2 first_feedback_coefficients = float2(coefficients.first_feedback_coefficients());
const float2 first_causal_feedforward_coefficients = float2(
coefficients.first_causal_feedforward_coefficients());
const float2 first_non_causal_feedforward_coefficients = float2(
coefficients.first_non_causal_feedforward_coefficients());
const float2 second_feedback_coefficients = float2(coefficients.second_feedback_coefficients());
const float2 second_causal_feedforward_coefficients = float2(
coefficients.second_causal_feedforward_coefficients());
const float2 second_non_causal_feedforward_coefficients = float2(
coefficients.second_non_causal_feedforward_coefficients());
const float first_causal_boundary_coefficient = float(
coefficients.first_causal_boundary_coefficient());
const float first_non_causal_boundary_coefficient = float(
coefficients.first_non_causal_boundary_coefficient());
const float second_causal_boundary_coefficient = float(
coefficients.second_causal_boundary_coefficient());
const float second_non_causal_boundary_coefficient = float(
coefficients.second_non_causal_boundary_coefficient());
const Domain domain = input.domain();
first_causal_output.allocate_texture(domain);
first_non_causal_output.allocate_texture(domain);
second_causal_output.allocate_texture(domain);
second_non_causal_output.allocate_texture(domain);
/* The first dispatch dimension is 4 dispatches, one for the first causal filter, one for the
* first non causal filter, one for the second causal filter, and one for the second non causal
* filter. */
const int2 parallel_for_size = int2(4, domain.data_size.y);
/* Blur the input horizontally by applying a fourth order IIR filter approximating a Gaussian
* filter using Van Vliet's design method. This is based on the following paper:
*
* Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
* filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
* 98EX170). Vol. 1. IEEE, 1998.
*
* We decomposed the filter into two second order filters, so we actually run four filters per
* row in parallel, one for the first causal filter, one for the first non causal filter, one for
* the second causal filter, and one for the second non causal filter, storing the result of each
* separately. See the VanVlietGaussianCoefficients class and the implementation for more
* information. */
parallel_for(parallel_for_size, [&](const int2 invocation) {
/* The shader runs parallel across rows but serially across columns. */
int y = invocation.y;
int width = input.domain().data_size.x;
/* The second dispatch dimension is four dispatches:
*
* 0 -> First causal filter.
* 1 -> First non causal filter.
* 2 -> Second causal filter.
* 3 -> Second non causal filter.
*
* We detect causality by even numbers. */
bool is_causal = invocation.x % 2 == 0;
float2 first_feedforward_coefficients = is_causal ? first_causal_feedforward_coefficients :
first_non_causal_feedforward_coefficients;
float first_boundary_coefficient = is_causal ? first_causal_boundary_coefficient :
first_non_causal_boundary_coefficient;
float2 second_feedforward_coefficients = is_causal ?
second_causal_feedforward_coefficients :
second_non_causal_feedforward_coefficients;
float second_boundary_coefficient = is_causal ? second_causal_boundary_coefficient :
second_non_causal_boundary_coefficient;
/* And we detect the filter by order. */
bool is_first_filter = invocation.x < 2;
float2 feedforward_coefficients = is_first_filter ? first_feedforward_coefficients :
second_feedforward_coefficients;
float2 feedback_coefficients = is_first_filter ? first_feedback_coefficients :
second_feedback_coefficients;
float boundary_coefficient = is_first_filter ? first_boundary_coefficient :
second_boundary_coefficient;
/* Create an array that holds the last FILTER_ORDER inputs along with the current input. The
* current input is at index 0 and the oldest input is at index FILTER_ORDER. We assume Neumann
* boundary condition, so we initialize all inputs by the boundary pixel. */
int2 boundary_texel = is_causal ? int2(0, y) : int2(width - 1, y);
float4 input_boundary = float4(input.load_pixel<Color>(boundary_texel));
float4 inputs[FILTER_ORDER + 1] = {input_boundary, input_boundary, input_boundary};
/* Create an array that holds the last FILTER_ORDER outputs along with the current output. The
* current output is at index 0 and the oldest output is at index FILTER_ORDER. We assume
* Neumann boundary condition, so we initialize all outputs by the boundary pixel multiplied by
* the boundary coefficient. See the VanVlietGaussianCoefficients class for more information on
* the boundary handing. */
float4 output_boundary = input_boundary * boundary_coefficient;
float4 outputs[FILTER_ORDER + 1] = {output_boundary, output_boundary, output_boundary};
for (int x = 0; x < width; x++) {
/* Run forward across rows for the causal filter and backward for the non causal filter. */
int2 texel = is_causal ? int2(x, y) : int2(width - 1 - x, y);
inputs[0] = float4(input.load_pixel<Color>(texel));
/* Compute the filter based on its difference equation, this is not in the Van Vliet paper
* because the filter was decomposed, but it is essentially similar to Equation (28) for the
* causal filter or Equation (29) for the non causal filter in Deriche's paper, except it is
* second order, not fourth order.
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss.
* INRIA, 1993.
*
* The only difference is that the non causal filter ignores the current value and starts
* from the previous input, as can be seen in the subscript of the first input term in both
* equations. So add one while indexing the non causal inputs. */
outputs[0] = float4(0.0f);
int first_input_index = is_causal ? 0 : 1;
for (int i = 0; i < FILTER_ORDER; i++) {
outputs[0] += feedforward_coefficients[i] * inputs[first_input_index + i];
outputs[0] -= feedback_coefficients[i] * outputs[i + 1];
}
/* Store the causal and non causal outputs of each of the two filters independently, then sum
* them in a separate shader dispatch for better parallelism. */
if (is_causal) {
if (is_first_filter) {
first_causal_output.store_pixel(texel, Color(outputs[0]));
}
else {
second_causal_output.store_pixel(texel, Color(outputs[0]));
}
}
else {
if (is_first_filter) {
first_non_causal_output.store_pixel(texel, Color(outputs[0]));
}
else {
second_non_causal_output.store_pixel(texel, Color(outputs[0]));
}
}
/* Shift the inputs temporally by one. The oldest input is discarded, while the current input
* will retain its value but will be overwritten with the new current value in the next
* iteration. */
for (int i = FILTER_ORDER; i >= 1; i--) {
inputs[i] = inputs[i - 1];
}
/* Shift the outputs temporally by one. The oldest output is discarded, while the current
* output will retain its value but will be overwritten with the new current value in the
* next iteration. */
for (int i = FILTER_ORDER; i >= 1; i--) {
outputs[i] = outputs[i - 1];
}
}
});
}
static void blur_pass(Context &context, const Result &input, Result &output, const float sigma)
{
Result first_causal_result = context.create_result(ResultType::Color);
Result first_non_causal_result = context.create_result(ResultType::Color);
Result second_causal_result = context.create_result(ResultType::Color);
Result second_non_causal_result = context.create_result(ResultType::Color);
if (context.use_gpu()) {
blur_pass_gpu(context,
input,
first_causal_result,
first_non_causal_result,
second_causal_result,
second_non_causal_result,
sigma);
}
else {
blur_pass_cpu(context,
input,
first_causal_result,
first_non_causal_result,
second_causal_result,
second_non_causal_result,
sigma);
}
sum_causal_and_non_causal_results(context,
first_causal_result,
first_non_causal_result,
second_causal_result,
second_non_causal_result,
output);
first_causal_result.release();
first_non_causal_result.release();
second_causal_result.release();
second_non_causal_result.release();
}
/* Computes the inverse of compute_sigma_from_radius in recursive_gaussian_blur.cc, see that
* function for more information. */
static float2 compute_radius_from_sigma(const float2 sigma)
{
return sigma * 3.0f;
}
void van_vliet_gaussian_blur(Context &context,
const Result &input,
Result &output,
const float2 &sigma,
const bool extend_bounds)
{
BLI_assert_msg(math::reduce_max(sigma) >= 32.0f,
"Van Vliet filter is less accurate for sigma values less than 32. Use Deriche "
"filter instead or direct convolution instead.");
if (extend_bounds) {
const int2 padding_size = int2(math::ceil(compute_radius_from_sigma(sigma)));
Result padded_input = context.create_result(input.type());
pad(context, input, padded_input, int2(padding_size.x, 0), PaddingMethod::Zero);
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, padded_input, horizontal_pass_result, sigma.x);
padded_input.release();
Result padded_horizontal_pass_result = context.create_result(input.type());
pad(context,
horizontal_pass_result,
padded_horizontal_pass_result,
int2(padding_size.y, 0),
PaddingMethod::Zero);
horizontal_pass_result.release();
blur_pass(context, padded_horizontal_pass_result, output, sigma.y);
padded_horizontal_pass_result.release();
}
else {
Result horizontal_pass_result = context.create_result(input.type());
blur_pass(context, input, horizontal_pass_result, sigma.x);
blur_pass(context, horizontal_pass_result, output, sigma.y);
horizontal_pass_result.release();
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,97 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Bokeh Kernel Key.
*/
class BokehKernelKey {
public:
int2 size;
int sides;
float rotation;
float roundness;
float catadioptric;
float lens_shift;
BokehKernelKey(
int2 size, int sides, float rotation, float roundness, float catadioptric, float lens_shift);
uint64_t hash() const;
};
bool operator==(const BokehKernelKey &a, const BokehKernelKey &b);
/* -------------------------------------------------------------------------------------------------
* Bokeh Kernel.
*
* A cached resource that computes and caches a result containing the unnormalized convolution
* kernel, which when convolved with an image emulates a bokeh lens with the given parameters. */
class BokehKernel : public CachedResource {
public:
Result result;
BokehKernel(Context &context,
int2 size,
int sides,
float rotation,
float roundness,
float catadioptric,
float lens_shift);
~BokehKernel();
private:
void compute_gpu(Context &context,
const int sides,
const float rotation,
const float roundness,
const float catadioptric,
const float lens_shift);
void compute_cpu(const int sides,
const float rotation,
const float roundness,
const float catadioptric,
const float lens_shift);
};
/* ------------------------------------------------------------------------------------------------
* Bokeh Kernel Container.
*/
class BokehKernelContainer : CachedResourceContainer {
private:
Map<BokehKernelKey, std::unique_ptr<BokehKernel>> map_;
public:
void reset() override;
/* Check if there is an available BokehKernel cached resource with the given parameters in the
* container, if one exists, return it, otherwise, return a newly created one and add it to the
* container. In both cases, tag the cached resource as needed to keep it cached for the next
* evaluation. */
Result &get(Context &context,
int2 size,
int sides,
float rotation,
float roundness,
float catadioptric,
float lens_shift);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,95 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "GPU_texture.hh"
#include "DNA_image_types.h"
#include "RE_pipeline.h"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Cached Image Key.
*/
class CachedImageKey {
public:
const int layer_index;
const std::string pass_name;
const std::string view_name;
const int frame;
CachedImageKey(const int layer_index,
const std::string pass_name,
const std::string view_name,
const int frame);
uint64_t hash() const;
};
bool operator==(const CachedImageKey &a, const CachedImageKey &b);
/* -------------------------------------------------------------------------------------------------
* Cached Image.
*
* A cached resource that computes and caches a result containing the contents of the image with
* the given image user. */
class CachedImage : public CachedResource {
public:
Result result;
private:
/* For GPU, the result wraps an external GPU texture that is generated by the IMB module and
* stored in this member to be freed when the cached resource is deleted. */
gpu::Texture *texture_ = nullptr;
public:
CachedImage(Context &context, Image &image, ImageUser &image_user, const char *pass_name);
~CachedImage();
private:
/* Populates the Cryptomatte meta data of the image. */
void populate_cryptomatte_meta_data(const RenderResult *render_result,
const ImageUser &image_user);
/* Populates the meta data of the image. */
void populate_meta_data(const ImBuf *image_buffer);
};
/* ------------------------------------------------------------------------------------------------
* Cached Image Container.
*/
class CachedImageContainer : CachedResourceContainer {
private:
Map<std::string, Map<CachedImageKey, std::unique_ptr<CachedImage>>> map_;
/* A map that stores the update counts of the images at the moment they were cached. */
Map<std::string, uint64_t> update_counts_;
public:
void reset() override;
/* Check if the given image has changed since it was cached, and if so, invalidate its cache
* entry. Then, check if there is an available CachedImage cached resource with the given image
* user and pass_name in the container, if one exists, return it, otherwise, return a newly
* created one and add it to the container. In both cases, tag the cached resource as needed to
* keep it cached for the next evaluation. */
Result &get(Context &context, Image &image, const ImageUser &image_user, const char *pass_name);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,102 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_mask_types.h"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Cached Mask Key.
*/
class CachedMaskKey {
public:
int2 data_size;
int2 display_size;
int2 data_offset;
float aspect_ratio;
bool use_feather;
bool srgb_to_linear;
int frame;
int motion_blur_samples;
float motion_blur_shutter;
CachedMaskKey(const Domain &domain,
float aspect_ratio,
bool use_feather,
bool srgb_to_linear,
int frame,
int motion_blur_samples,
float motion_blur_shutter);
uint64_t hash() const;
friend bool operator==(const CachedMaskKey &a, const CachedMaskKey &b) = default;
};
/* -------------------------------------------------------------------------------------------------
* Cached Mask.
*
* A cached resource that computes and caches a result containing the result of evaluating the
* given mask ID on a space that spans the given size, parameterized by the given parameters. */
class CachedMask : public CachedResource {
public:
Result result;
CachedMask(Context &context,
Mask *mask,
const Domain &domain,
int frame,
float aspect_ratio,
bool use_feather,
int motion_blur_samples,
float motion_blur_shutter,
bool srgb_to_linear);
~CachedMask();
};
/* ------------------------------------------------------------------------------------------------
* Cached Mask Container.
*/
class CachedMaskContainer : CachedResourceContainer {
private:
Map<std::string, Map<CachedMaskKey, std::unique_ptr<CachedMask>>> map_;
/* A map that stores the update counts of the masks at the moment they were cached. */
Map<std::string, uint64_t> update_counts_;
public:
void reset() override;
/* Check if the given mask ID has changed since the last time it was retrieved through its
* recalculate flag, and if so, invalidate its corresponding cached mask and reset the
* recalculate flag to ready it to track the next change. Then, check if there is an available
* CachedMask cached resource with the given parameters in the container, if one exists, return
* it, otherwise, return a newly created one and add it to the container. In both cases, tag the
* cached resource as needed to keep it cached for the next evaluation. */
Result &get(Context &context,
Mask *mask,
const Domain &domain,
float aspect_ratio,
bool use_feather,
int frame,
int motion_blur_samples,
float motion_blur_shutter,
bool srgb_to_linear);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,52 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender::compositor {
/* -------------------------------------------------------------------------------------------------
* Cached Resource.
*
* A cached resource is any resource that can be cached across compositor evaluations and across
* multiple operations. Cached resources are managed by an instance of a StaticCacheManager, stored
* in an instance of a CachedResourceContainer, and are freed when they are no longer needed, a
* state which is represented by the `needed` member in the class. For more information on the
* caching mechanism, see the StaticCacheManager class.
*
* To add a new cached resource:
*
* - Create a key class that can be used to identify the resource in a Map if needed.
* - Create a derived class from CachedResource to represent the resource.
* - Create a derived class from CachedResourceContainer to store the resources.
* - Add an instance of the container to StaticCacheManager and call its reset method.
*
* See the existing cached resources for reference. */
class CachedResource {
public:
/* A flag that represents the needed status of the cached resource. See the StaticCacheManager
* class for more information on how this member is utilized in the caching mechanism. */
bool needed = true;
};
/* -------------------------------------------------------------------------------------------------
* Cached Resource Container.
*
* A cached resource container stores all the cached resources for a specific cached resource type.
* The cached resources are typically stored in a map identified by a key type. The reset method
* should be implemented as described in StaticCacheManager::reset. An appropriate getter method
* should be provided that properly sets the CachedResource::needed flag as described in the
* description of the StaticCacheManager class.
*
* See the existing cached resources for reference. */
class CachedResourceContainer {
public:
/* Reset the container by deleting the cached resources that are no longer needed because they
* weren't used in the last evaluation and prepare the remaining cached resources to track their
* needed status in the next evaluation. See the description of the StaticCacheManager class for
* more information. This should be called in StaticCacheManager::reset. */
virtual void reset() = 0;
};
} // namespace blender::compositor

View File

@@ -0,0 +1,70 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "GPU_shader.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Cached Shader Key.
*/
class CachedShaderKey {
public:
std::string info_name;
ResultPrecision precision;
CachedShaderKey(const char *info_name, ResultPrecision precision);
uint64_t hash() const;
};
bool operator==(const CachedShaderKey &a, const CachedShaderKey &b);
/* -------------------------------------------------------------------------------------------------
* Cached Shader.
*
* A cached resource that constructs and caches a GPU shader from the given info name with its
* output images' precision changed to the given precision. */
class CachedShader : public CachedResource {
private:
gpu::Shader *shader_ = nullptr;
public:
CachedShader(const char *info_name, ResultPrecision precision);
~CachedShader();
gpu::Shader *shader() const;
};
/* ------------------------------------------------------------------------------------------------
* Cached Shader Container.
*/
class CachedShaderContainer : public CachedResourceContainer {
private:
Map<CachedShaderKey, std::unique_ptr<CachedShader>> map_;
public:
void reset() override;
/* Check if there is an available CachedShader cached resource with the given parameters in the
* container, if one exists, return its shader, otherwise, return the shader of a newly created
* one and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
gpu::Shader *get(const char *info_name, ResultPrecision precision);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Deriche Gaussian Coefficients Key.
*/
class DericheGaussianCoefficientsKey {
public:
float sigma;
DericheGaussianCoefficientsKey(float sigma);
uint64_t hash() const;
};
bool operator==(const DericheGaussianCoefficientsKey &a, const DericheGaussianCoefficientsKey &b);
/* -------------------------------------------------------------------------------------------------
* Deriche Gaussian Coefficients.
*
* A caches resource that computes and caches the coefficients of the fourth order IIR filter
* approximating a Gaussian filter computed using Deriche's design method. This is based on the
* following paper:
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss. INRIA,
* 1993.
*/
class DericheGaussianCoefficients : public CachedResource {
private:
/* The d_ii coefficients in Equation (28) and (29). Those are the same for the causal and non
* causal filters as can be seen in Equation (31). */
double4 feedback_coefficients_;
/* The n_ii^+ coefficients in Equation (28). */
double4 causal_feedforward_coefficients_;
/* The n_ii^- coefficients in Equation (29). */
double4 non_causal_feedforward_coefficients_;
/* The difference equation in Equation (28) rely on previous outputs to compute the new output,
* and those previous outputs need to be properly initialized somehow. To do Neumann boundary
* condition, we multiply the boundary value with this coefficient to simulate an infinite stream
* of the boundary value. See the implementation for more information. */
double causal_boundary_coefficient_;
/* Same as causal_boundary_coefficient_ but for the non causal filter. */
double non_causal_boundary_coefficient_;
public:
DericheGaussianCoefficients(Context &context, float sigma);
const double4 &feedback_coefficients() const;
const double4 &causal_feedforward_coefficients() const;
const double4 &non_causal_feedforward_coefficients() const;
double causal_boundary_coefficient() const;
double non_causal_boundary_coefficient() const;
};
/* ------------------------------------------------------------------------------------------------
* Deriche Gaussian Coefficients Container.
*/
class DericheGaussianCoefficientsContainer : CachedResourceContainer {
private:
Map<DericheGaussianCoefficientsKey, std::unique_ptr<DericheGaussianCoefficients>> map_;
public:
void reset() override;
/* Check if there is an available DericheGaussianCoefficients cached resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
DericheGaussianCoefficients &get(Context &context, float sigma);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,89 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_movieclip_types.h"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
enum class DistortionType : uint8_t {
Undistort = 0,
Distort = 1,
};
/* ------------------------------------------------------------------------------------------------
* Distortion Grid Key.
*/
class DistortionGridKey {
public:
MovieTrackingCamera camera;
Domain domain;
DistortionType type;
int2 calibration_size;
DistortionGridKey(const MovieTrackingCamera &camera,
Domain domain,
DistortionType type,
int2 calibration_size);
uint64_t hash() const;
};
bool operator==(const DistortionGridKey &a, const DistortionGridKey &b);
/* -------------------------------------------------------------------------------------------------
* Distortion Grid.
*
* A cached resource that computes and caches a result containing the normalized coordinates after
* applying the camera distortion of a given movie clip tracking camera. See the constructor for
* more information. */
class DistortionGrid : public CachedResource {
public:
Result result;
/* The calibration size is the size of the image where the tracking camera was calibrated, this
* is the size of the movie clip in most cases. */
DistortionGrid(Context &context,
MovieClip *movie_clip,
Domain domain,
DistortionType type,
int2 calibration_size);
~DistortionGrid();
};
/* ------------------------------------------------------------------------------------------------
* Distortion Grid Container.
*/
class DistortionGridContainer : CachedResourceContainer {
private:
Map<DistortionGridKey, std::unique_ptr<DistortionGrid>> map_;
public:
void reset() override;
/* Check if there is an available DistortionGrid cached resource with the given parameters in the
* container, if one exists, return it, otherwise, return a newly created one and add it to the
* container. In both cases, tag the cached resource as needed to keep it cached for the next
* evaluation. */
Result &get(Context &context,
MovieClip *movie_clip,
Domain domain,
DistortionType type,
int frame_number);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,77 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <complex>
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_angle_types.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Fog Glow Kernel Key.
*/
class FogGlowKernelKey {
public:
int kernel_size;
int2 spatial_size;
math::AngleRadian field_of_view;
FogGlowKernelKey(int kernel_size, int2 spatial_size, math::AngleRadian field_of_view);
uint64_t hash() const;
};
bool operator==(const FogGlowKernelKey &a, const FogGlowKernelKey &b);
/* -------------------------------------------------------------------------------------------------
* Fog Glow Kernel.
*
* A cached resource that computes and caches a Fog Glow convolution kernel in the frequency domain
* using FFTW's real to complex transform. The kernel is computed within a specific kernel size but
* zero padded to match a certain spatial size. */
class FogGlowKernel : public CachedResource {
private:
/* The normalization factor that should be used to normalize the kernel frequencies. See the
* implementation for more information. */
float normalization_factor_ = 1.0f;
/* The kernel in the frequency domain. See the implementation for more information. */
std::complex<float> *frequencies_ = nullptr;
public:
FogGlowKernel(int kernel_size, int2 spatial_size, math::AngleRadian field_of_view);
~FogGlowKernel();
std::complex<float> *frequencies() const;
float normalization_factor() const;
};
/* ------------------------------------------------------------------------------------------------
* Fog Glow Kernel Container.
*/
class FogGlowKernelContainer : CachedResourceContainer {
private:
Map<FogGlowKernelKey, std::unique_ptr<FogGlowKernel>> map_;
public:
void reset() override;
/* Check if there is an available FogGlowKernel cached resource with the given parameters in the
* container, if one exists, return it, otherwise, return a newly created one and add it to the
* container. In both cases, tag the cached resource as needed to keep it cached for the next
* evaluation. */
FogGlowKernel &get(int kernel_size, int2 spatial_size, math::AngleRadian field_of_view);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
enum class CoordinatesType : uint8_t {
Uniform,
Normalized,
Pixel,
};
/* ------------------------------------------------------------------------------------------------
* Image Coordinates Key.
*/
class ImageCoordinatesKey {
public:
const int2 data_size;
const int2 display_size;
const int2 data_offset;
const CoordinatesType type;
ImageCoordinatesKey(const Domain &domain, const CoordinatesType type);
uint64_t hash() const;
};
bool operator==(const ImageCoordinatesKey &a, const ImageCoordinatesKey &b);
/* -------------------------------------------------------------------------------------------------
* Image Coordinates.
*
* A cached resource that computes and caches a result containing the coordinates of the pixels of
* an image with the given domain. */
class ImageCoordinates : public CachedResource {
public:
Result result;
ImageCoordinates(Context &context, const Domain &domain, const CoordinatesType type);
~ImageCoordinates();
private:
void compute_gpu(Context &context, const CoordinatesType type);
void compute_cpu(const CoordinatesType type);
};
/* ------------------------------------------------------------------------------------------------
* Image Coordinates Container.
*/
class ImageCoordinatesContainer : CachedResourceContainer {
private:
Map<ImageCoordinatesKey, std::unique_ptr<ImageCoordinates>> map_;
public:
void reset() override;
/* Check if there is an available ImageCoordinates cached resource with the given parameters in
* the container, if one exists, return it, otherwise, return a newly created one and add it to
* the container. In both cases, tag the cached resource as needed to keep it cached for the next
* evaluation. */
Result &get(Context &context, const Domain &domain, const CoordinatesType type);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_movieclip_types.h"
#include "DNA_tracking_types.h"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Keying Screen Key.
*/
class KeyingScreenKey {
public:
int2 frame;
float smoothness;
KeyingScreenKey(int frame, float smoothness);
uint64_t hash() const;
};
bool operator==(const KeyingScreenKey &a, const KeyingScreenKey &b);
/* -------------------------------------------------------------------------------------------------
* Keying Screen.
*
* A cached resource that computes and caches a result containing the keying screen computed by
* interpolating the markers of the given movie tracking object in the given movie clip. */
class KeyingScreen : public CachedResource {
public:
Result result;
KeyingScreen(Context &context,
MovieClip *movie_clip,
MovieTrackingObject *movie_tracking_object,
const float smoothness);
~KeyingScreen();
private:
void compute_gpu(Context &context,
const float smoothness,
Vector<float2> &marker_positions,
const Vector<float4> &marker_colors);
void compute_cpu(const float smoothness,
const Vector<float2> &marker_positions,
const Vector<float4> &marker_colors);
};
/* ------------------------------------------------------------------------------------------------
* Keying Screen Container.
*/
class KeyingScreenContainer : CachedResourceContainer {
private:
Map<std::string, Map<KeyingScreenKey, std::unique_ptr<KeyingScreen>>> map_;
/* A map that stores the update counts of the keying screens at the moment they were cached. */
Map<std::string, uint64_t> update_counts_;
public:
void reset() override;
/* Check if the given movie clip ID has changed since the last time it was retrieved through its
* recalculate flag, and if so, invalidate its corresponding cached keying screens and reset the
* recalculate flag to ready it to track the next change. Then, check if there is an available
* KeyingScreen cached resource with the given parameters in the container, if one exists, return
* it, otherwise, return a newly created one and add it to the container. In both cases, tag the
* cached resource as needed to keep it cached for the next evaluation. */
Result &get(Context &context,
MovieClip *movie_clip,
MovieTrackingObject *movie_tracking_object,
float smoothness);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,76 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Morphological Distance Feather Key.
*/
class MorphologicalDistanceFeatherWeightsKey {
public:
int type;
float radius;
MorphologicalDistanceFeatherWeightsKey(int type, float radius);
uint64_t hash() const;
};
bool operator==(const MorphologicalDistanceFeatherWeightsKey &a,
const MorphologicalDistanceFeatherWeightsKey &b);
/* -------------------------------------------------------------------------------------------------
* Morphological Distance Feather Weights.
*
* A cached resource that computes and caches 1D GPU textures containing the weights of the
* separable Gaussian filter of the given radius as well as an inverse distance falloff of the
* given type and radius. The weights and falloffs are symmetric, because the Gaussian and falloff
* functions are all even functions. Consequently, only the positive half of the filter is computed
* and the shader takes that into consideration. */
class MorphologicalDistanceFeatherWeights : public CachedResource {
public:
Result weights;
Result falloffs;
MorphologicalDistanceFeatherWeights(Context &context, int type, int radius);
~MorphologicalDistanceFeatherWeights();
private:
Result compute_weights(Context &context, int radius);
Result compute_distance_falloffs(Context &context, int type, int radius);
};
/* ------------------------------------------------------------------------------------------------
* Morphological Distance Feather Key.
*/
class MorphologicalDistanceFeatherWeightsContainer : CachedResourceContainer {
private:
Map<MorphologicalDistanceFeatherWeightsKey, std::unique_ptr<MorphologicalDistanceFeatherWeights>>
map_;
public:
void reset() override;
/* Check if there is an available MorphologicalDistanceFeatherWeights cached resource with the
* given parameters in the container, if one exists, return it, otherwise, return a newly created
* one and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
MorphologicalDistanceFeatherWeights &get(Context &context, int type, int radius);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,153 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <string>
#include "BLI_map.hh"
#include "DNA_color_types.h"
#include "COM_cached_resource.hh"
namespace blender {
namespace gpu {
class Shader;
} // namespace gpu
namespace compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* OCIO Color Space Conversion Shader Key.
*/
class OCIOColorSpaceConversionShaderKey {
public:
std::string source;
std::string target;
std::string config_cache_id;
OCIOColorSpaceConversionShaderKey(const std::string &source,
const std::string &target,
const std::string &config_cache_id);
uint64_t hash() const;
};
bool operator==(const OCIOColorSpaceConversionShaderKey &a,
const OCIOColorSpaceConversionShaderKey &b);
class GPUShaderCreator;
/* -------------------------------------------------------------------------------------------------
* OCIO Color Space Conversion Shader.
*
* A cached resource that creates and caches a GPU shader that converts the source OCIO color space
* of an image into a different target OCIO color space. */
class OCIOColorSpaceConversionShader : public CachedResource {
private:
std::shared_ptr<GPUShaderCreator> shader_creator_;
public:
OCIOColorSpaceConversionShader(Context &context, std::string source, std::string target);
gpu::Shader *bind_shader_and_resources();
void unbind_shader_and_resources();
const char *input_sampler_name();
const char *output_image_name();
};
/* ------------------------------------------------------------------------------------------------
* OCIO Color Space Conversion Shader Container.
*/
class OCIOColorSpaceConversionShaderContainer : CachedResourceContainer {
private:
Map<OCIOColorSpaceConversionShaderKey, std::unique_ptr<OCIOColorSpaceConversionShader>> map_;
public:
void reset() override;
/* Check if there is an available OCIOColorSpaceConversionShader cached resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
OCIOColorSpaceConversionShader &get(Context &context, std::string source, std::string target);
};
/* ------------------------------------------------------------------------------------------------
* OCIO To Display Shader Key.
*/
class OCIOToDisplayShaderKey {
public:
std::string display_device;
std::string view_transform;
std::string look;
bool inverse;
std::string config_cache_id;
OCIOToDisplayShaderKey(const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse,
const std::string &config_cache_id);
uint64_t hash() const;
};
bool operator==(const OCIOToDisplayShaderKey &a, const OCIOToDisplayShaderKey &b);
class GPUShaderCreator;
/* -------------------------------------------------------------------------------------------------
* OCIO To Display Shader.
*
* A cached resource that creates and caches a GPU shader that converts the source OCIO color space
* of an image into a different target OCIO color space. */
class OCIOToDisplayShader : public CachedResource {
private:
std::shared_ptr<GPUShaderCreator> shader_creator_;
public:
OCIOToDisplayShader(Context &context,
const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse);
gpu::Shader *bind_shader_and_resources();
void unbind_shader_and_resources();
const char *input_sampler_name();
const char *output_image_name();
};
/* ------------------------------------------------------------------------------------------------
* OCIO To Display Shader Container.
*/
class OCIOToDisplayShaderContainer : CachedResourceContainer {
private:
Map<OCIOToDisplayShaderKey, std::unique_ptr<OCIOToDisplayShader>> map_;
public:
void reset() override;
/* Check if there is an available OCIOToDisplayShader cached resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
OCIOToDisplayShader &get(Context &context,
const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse);
};
} // namespace compositor
} // namespace blender

View File

@@ -0,0 +1,71 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* -------------------------------------------------------------------------------------------------
* SMAA Precomputed Textures.
*
* A cached resource that caches the precomputed textures needed by the SMAA algorithm. The
* precomputed textures are constants, so this is a parameterless cached resource. */
class SMAAPrecomputedTextures : public CachedResource {
public:
/* CPU storage, unused for GPU execution device. We can't store the GPU textures in the result
* because it requires special data types that are not supported by the Result class. */
Result search_texture;
Result area_texture;
private:
/* GPU storage, unused for CPU execution device. */
gpu::Texture *search_texture_ = nullptr;
gpu::Texture *area_texture_ = nullptr;
public:
SMAAPrecomputedTextures(Context &context);
~SMAAPrecomputedTextures();
void bind_search_texture(gpu::Shader *shader, const char *sampler_name) const;
void unbind_search_texture() const;
void bind_area_texture(gpu::Shader *shader, const char *sampler_name) const;
void unbind_area_texture() const;
private:
void compute_gpu();
void compute_cpu();
};
/* ------------------------------------------------------------------------------------------------
* SMAA Precomputed Textures Container.
*/
class SMAAPrecomputedTexturesContainer : public CachedResourceContainer {
private:
std::unique_ptr<SMAAPrecomputedTextures> textures_;
public:
void reset() override;
/* Check if a cached SMAA precomputed texture exists, if it does, return it, otherwise, return
* a newly created one and store it in the container. In both cases, tag the cached resource as
* needed to keep it cached for the next evaluation. */
SMAAPrecomputedTextures &get(Context &context);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,104 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include "BLI_map.hh"
#include "DNA_node_types.h"
#include "DNA_vfont_types.h"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
enum class HorizontalAlignment : uint8_t {
Left = 0,
Center = 1,
Right = 2,
};
enum class VerticalAlignment : uint8_t {
Top = 0,
TopBaseline = 1,
Middle = 2,
BottomBaseline = 3,
Bottom = 4,
};
/* ------------------------------------------------------------------------------------------------
* String Image Key.
*/
class StringImageKey {
public:
const std::string string;
const VFont *font;
const float size;
const HorizontalAlignment horizontal_alignment;
const VerticalAlignment vertical_alignment;
const std::optional<int> wrap_width;
StringImageKey(const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width);
uint64_t hash() const;
friend bool operator==(const StringImageKey &a, const StringImageKey &b) = default;
};
/* -------------------------------------------------------------------------------------------------
* String Image.
*
* A cached resource that computes and caches a result containing a string with the given
* parameters. */
class StringImage : public CachedResource {
public:
Result result;
StringImage(Context &context,
const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width);
~StringImage();
};
/* ------------------------------------------------------------------------------------------------
* String Image Container.
*/
class StringImageContainer : CachedResourceContainer {
private:
Map<StringImageKey, std::unique_ptr<StringImage>> map_;
public:
void reset() override;
/* Check if there is an available StringImage cached resource with the given parameters in the
* container, if one exists, return it, otherwise, return a newly created one and add it to the
* container. In both cases, tag the cached resource as needed to keep it cached for the next
* evaluation. */
Result &get(Context &context,
const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,69 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_filter.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Symmetric Blur Weights Key.
*/
class SymmetricBlurWeightsKey {
public:
math::FilterKernel type;
float2 radius;
SymmetricBlurWeightsKey(math::FilterKernel type, float2 radius);
uint64_t hash() const;
};
bool operator==(const SymmetricBlurWeightsKey &a, const SymmetricBlurWeightsKey &b);
/* -------------------------------------------------------------------------------------------------
* Symmetric Blur Weights.
*
* A cached resource that computes and caches a result containing the weights of the filter of the
* given type and radius. The filter is assumed to be symmetric, because the filter functions are
* evaluated on the normalized distance to the center. Consequently, only the upper right quadrant
* are computed and the user takes that into consideration. */
class SymmetricBlurWeights : public CachedResource {
public:
Result weights;
SymmetricBlurWeights(Context &context, math::FilterKernel type, float2 radius);
~SymmetricBlurWeights();
};
/* ------------------------------------------------------------------------------------------------
* Symmetric Blur Weights Container.
*/
class SymmetricBlurWeightsContainer : public CachedResourceContainer {
private:
Map<SymmetricBlurWeightsKey, std::unique_ptr<SymmetricBlurWeights>> map_;
public:
void reset() override;
/* Check if there is an available SymmetricBlurWeights cached resource with the given parameters
* in the container, if one exists, return it, otherwise, return a newly created one and add it
* to the container. In both cases, tag the cached resource as needed to keep it cached for the
* next evaluation. */
Result &get(Context &context, math::FilterKernel type, float2 radius);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,79 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_filter.hh"
#include "COM_cached_resource.hh"
#include "COM_result.hh"
namespace blender::compositor {
class Context;
/* -------------------------------------------------------------------- */
/** \name Symmetric Separable Blur Weights Key
* \{ */
class SymmetricSeparableBlurWeightsKey {
public:
math::FilterKernel type;
float radius;
SymmetricSeparableBlurWeightsKey(math::FilterKernel type, float radius);
uint64_t hash() const;
};
bool operator==(const SymmetricSeparableBlurWeightsKey &a,
const SymmetricSeparableBlurWeightsKey &b);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Symmetric Separable Blur Weights
*
* A cached resource that computes and caches a 1D GPU texture containing the weights of the
* separable filter of the given type and radius. The filter is assumed to be symmetric, because
* the filter functions are all even functions. Consequently, only the positive half of the filter
* is computed and the shader takes that into consideration.
* \{ */
class SymmetricSeparableBlurWeights : public CachedResource {
public:
Result weights;
SymmetricSeparableBlurWeights(Context &context, math::FilterKernel type, float radius);
~SymmetricSeparableBlurWeights();
};
/** \} */
/* -------------------------------------------------------------------- */
/** \name Symmetric Separable Blur Weights Container
* \{ */
class SymmetricSeparableBlurWeightsContainer : public CachedResourceContainer {
private:
Map<SymmetricSeparableBlurWeightsKey, std::unique_ptr<SymmetricSeparableBlurWeights>> map_;
public:
void reset() override;
/* Check if there is an available SymmetricSeparableBlurWeights cached resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
Result &get(Context &context, math::FilterKernel type, float radius);
};
/** \} */
} // namespace blender::compositor

View File

@@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <cstdint>
#include <memory>
#include "BLI_map.hh"
#include "BLI_math_vector_types.hh"
#include "COM_cached_resource.hh"
namespace blender::compositor {
class Context;
/* ------------------------------------------------------------------------------------------------
* Van Vliet Gaussian Coefficients Key.
*/
class VanVlietGaussianCoefficientsKey {
public:
float sigma;
VanVlietGaussianCoefficientsKey(float sigma);
uint64_t hash() const;
};
bool operator==(const VanVlietGaussianCoefficientsKey &a,
const VanVlietGaussianCoefficientsKey &b);
/* -------------------------------------------------------------------------------------------------
* Van Vliet Gaussian Coefficients.
*
* A caches resource that computes and caches the coefficients of the fourth order IIR filter
* approximating a Gaussian filter computed using Van Vliet's design method. This is based on the
* following paper:
*
* Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
* filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
* 98EX170). Vol. 1. IEEE, 1998.
*
* However, to improve the numerical stability of the filter, it is decomposed into a bank of
* two parallel second order IIR filters, each having a causal and a non causal filter. */
class VanVlietGaussianCoefficients : public CachedResource {
private:
/* The causal and non causal feedforward coefficients for the first second-order filter. */
double2 first_causal_feedforward_coefficients_;
double2 first_non_causal_feedforward_coefficients_;
/* The feedback coefficients for the first second order filter. This is the same for both the
* causal and non causal filters. */
double2 first_feedback_coefficients_;
/* The causal and non causal feedforward coefficients for the second second-order filter. */
double2 second_causal_feedforward_coefficients_;
double2 second_non_causal_feedforward_coefficients_;
/* The feedback coefficients for the second second-order filter. This is the same for both the
* causal and non causal filters. */
double2 second_feedback_coefficients_;
/* The difference equation of the IIR filter rely on previous outputs to compute the new output,
* and those previous outputs need to be properly initialized somehow. To do Neumann boundary
* condition, we multiply the boundary value with this coefficient to simulate an infinite stream
* of the boundary value. See the implementation for more information. */
double first_causal_boundary_coefficient_;
double first_non_causal_boundary_coefficient_;
double second_causal_boundary_coefficient_;
double second_non_causal_boundary_coefficient_;
public:
VanVlietGaussianCoefficients(Context &context, float sigma);
const double2 &first_causal_feedforward_coefficients() const;
const double2 &first_non_causal_feedforward_coefficients() const;
const double2 &first_feedback_coefficients() const;
const double2 &second_causal_feedforward_coefficients() const;
const double2 &second_non_causal_feedforward_coefficients() const;
const double2 &second_feedback_coefficients() const;
double first_causal_boundary_coefficient() const;
double first_non_causal_boundary_coefficient() const;
double second_causal_boundary_coefficient() const;
double second_non_causal_boundary_coefficient() const;
};
/* ------------------------------------------------------------------------------------------------
* Van Vliet Gaussian Coefficients Container.
*/
class VanVlietGaussianCoefficientsContainer : CachedResourceContainer {
private:
Map<VanVlietGaussianCoefficientsKey, std::unique_ptr<VanVlietGaussianCoefficients>> map_;
public:
void reset() override;
/* Check if there is an available VanVlietGaussianCoefficients cached resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. In both cases, tag the cached resource as needed to keep it
* cached for the next evaluation. */
VanVlietGaussianCoefficients &get(Context &context, float sigma);
};
} // namespace blender::compositor

View File

@@ -0,0 +1,310 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_bokeh_kernel.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Bokeh Kernel Key.
*/
BokehKernelKey::BokehKernelKey(
int2 size, int sides, float rotation, float roundness, float catadioptric, float lens_shift)
: size(size),
sides(sides),
rotation(rotation),
roundness(roundness),
catadioptric(catadioptric),
lens_shift(lens_shift)
{
}
uint64_t BokehKernelKey::hash() const
{
return get_default_hash(
size, size, get_default_hash(float4(rotation, roundness, catadioptric, lens_shift)));
}
bool operator==(const BokehKernelKey &a, const BokehKernelKey &b)
{
return a.size == b.size && a.sides == b.sides && a.rotation == b.rotation &&
a.roundness == b.roundness && a.catadioptric == b.catadioptric &&
a.lens_shift == b.lens_shift;
}
/* --------------------------------------------------------------------
* Bokeh Kernel.
*/
BokehKernel::BokehKernel(Context &context,
int2 size,
int sides,
float rotation,
float roundness,
float catadioptric,
float lens_shift)
: result(context.create_result(ResultType::Color))
{
this->result.allocate_texture(Domain(size), false);
if (context.use_gpu()) {
this->compute_gpu(context, sides, rotation, roundness, catadioptric, lens_shift);
}
else {
this->compute_cpu(sides, rotation, roundness, catadioptric, lens_shift);
}
}
BokehKernel::~BokehKernel()
{
this->result.release();
}
/* The exterior angle is the angle between each two consecutive vertices of the regular polygon
* from its center. */
static float compute_exterior_angle(int sides)
{
return (std::numbers::pi * 2.0f) / sides;
}
static float compute_rotation(float angle, int sides)
{
/* Offset the rotation such that the second vertex of the regular polygon lies on the positive
* y axis, which is 90 degrees minus the angle that it makes with the positive x axis assuming
* the first vertex lies on the positive x axis. */
const float offset = (std::numbers::pi / 2.0f) - compute_exterior_angle(sides);
return angle - offset;
}
void BokehKernel::compute_gpu(Context &context,
const int sides,
const float rotation,
const float roundness,
const float catadioptric,
const float lens_shift)
{
gpu::Shader *shader = context.get_shader("compositor_bokeh_image");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "exterior_angle", compute_exterior_angle(sides));
GPU_shader_uniform_1f(shader, "rotation", compute_rotation(rotation, sides));
GPU_shader_uniform_1f(shader, "roundness", roundness);
GPU_shader_uniform_1f(shader, "catadioptric", catadioptric);
GPU_shader_uniform_1f(shader, "lens_shift", lens_shift);
this->result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, this->result.domain().data_size);
this->result.unbind_as_image();
GPU_shader_unbind();
}
/* Get the 2D vertex position of the vertex with the given index in the regular polygon
* representing this bokeh. The polygon is rotated by the rotation amount and have a unit
* circumradius. The regular polygon is one whose vertices' exterior angles are given by
* exterior_angle. See the bokeh function for more information. */
static float2 get_regular_polygon_vertex_position(const int vertex_index,
const float exterior_angle,
const float rotation)
{
float angle = exterior_angle * vertex_index - rotation;
return float2(math::cos(angle), math::sin(angle));
}
/* Find the closest point to the given point on the given line. This assumes the length of the
* given line is not zero. */
static float2 closest_point_on_line(const float2 point,
const float2 line_start,
const float2 line_end)
{
float2 line_vector = line_end - line_start;
float2 point_vector = point - line_start;
float line_length_squared = math::dot(line_vector, line_vector);
float parameter = math::dot(point_vector, line_vector) / line_length_squared;
return line_start + line_vector * parameter;
}
/* Compute the value of the bokeh at the given point. The computed bokeh is essentially a regular
* polygon centered in space having the given circumradius. The regular polygon is one whose
* vertices' exterior angles are given by "exterior_angle", which relates to the number of vertices
* n through the equation "exterior angle = 2 pi / n". The regular polygon may additionally morph
* into a shape with the given properties:
*
* - The regular polygon may have a circular hole in its center whose radius is controlled by the
* "catadioptric" value.
* - The regular polygon is rotated by the "rotation" value.
* - The regular polygon can morph into a circle controlled by the "roundness" value, such that it
* becomes a full circle at unit roundness.
*
* The function returns 0 when the point lies inside the regular polygon and 1 otherwise. However,
* at the edges, it returns a narrow band gradient as a form of anti-aliasing. */
static float bokeh(const float2 point,
const float circumradius,
const float exterior_angle,
const float rotation,
const float roundness,
const float catadioptric)
{
if (circumradius == 0.0f) {
return 0.0f;
}
/* Get the index of the vertex of the regular polygon whose polar angle is maximum but less than
* the polar angle of the given point, taking rotation into account. This essentially finds the
* vertex closest to the given point in the clock-wise direction. */
float angle = math::mod_periodic(math::atan2(point.y, point.x) + rotation,
2.0f * std::numbers::pi_v<float>);
int vertex_index = int(angle / exterior_angle);
/* Compute the shortest distance between the origin and the polygon edge composed from the
* previously selected vertex and the one following it. */
float2 first_vertex = get_regular_polygon_vertex_position(
vertex_index, exterior_angle, rotation) *
circumradius;
float2 second_vertex = get_regular_polygon_vertex_position(
vertex_index + 1, exterior_angle, rotation) *
circumradius;
float2 closest_point = closest_point_on_line(point, first_vertex, second_vertex);
float distance_to_edge = math::length(closest_point);
/* Mix the distance to the edge with the circumradius, making it tend to the distance to a
* circle when roundness tends to 1. */
float distance_to_edge_round = math::interpolate(distance_to_edge, circumradius, roundness);
/* The point is outside of the bokeh, so we return 0. */
float distance = math::length(point);
if (distance > distance_to_edge_round) {
return 0.0f;
}
/* The point is inside the catadioptric hole and is not part of the bokeh, so we return 0. */
float catadioptric_distance = distance_to_edge_round * catadioptric;
if (distance < catadioptric_distance) {
return 0.0f;
}
/* The point is very close to the edge of the bokeh, so we return the difference between the
* distance to the edge and the distance as a form of anti-aliasing. */
if (distance_to_edge_round - distance < 1.0f) {
return distance_to_edge_round - distance;
}
/* The point is very close to the edge of the catadioptric hole, so we return the difference
* between the distance to the hole and the distance as a form of anti-aliasing. */
if (catadioptric != 0.0f && distance - catadioptric_distance < 1.0f) {
return distance - catadioptric_distance;
}
/* Otherwise, the point is part of the bokeh and we return 1. */
return 1.0f;
}
static float4 spectral_bokeh(const int2 texel,
const int2 size,
const float exterior_angle,
const float rotation,
const float roundness,
const float catadioptric,
const float lens_shift)
{
/* Since we need the regular polygon to occupy the entirety of the output image, the circumradius
* of the regular polygon is half the width of the output image. */
float circumradius = float(size.x) / 2.0f;
/* Move the texel coordinates such that the regular polygon is centered. */
float2 point = float2(texel) + float2(0.5f) - circumradius;
/* Each of the color channels of the output image contains a bokeh with a different circumradius.
* The largest one occupies the whole image as stated above, while the other two have circumradii
* that are shifted by an amount that is proportional to the "lens_shift" value. The alpha
* channel of the output is the average of all three values. */
float min_shift = math::abs(lens_shift * circumradius);
float min = bokeh(
point, circumradius - min_shift, exterior_angle, rotation, roundness, catadioptric);
float median_shift = min_shift / 2.0f;
float median = bokeh(
point, circumradius - median_shift, exterior_angle, rotation, roundness, catadioptric);
float max = bokeh(point, circumradius, exterior_angle, rotation, roundness, catadioptric);
float4 bokeh = float4(min, median, max, (max + median + min) / 3.0f);
/* If the lens shift is negative, swap the min and max bokeh values, which are stored in the red
* and blue channels respectively. Note that we take the absolute value of the lens shift above,
* so the sign of the lens shift only controls this swap. */
if (lens_shift < 0.0f) {
bokeh = float4(bokeh.z, bokeh.y, bokeh.x, bokeh.w);
}
return bokeh;
}
void BokehKernel::compute_cpu(const int sides,
const float rotation,
const float roundness,
const float catadioptric,
const float lens_shift)
{
const int2 size = this->result.domain().data_size;
const float exterior_angle = compute_exterior_angle(sides);
const float corrected_rotation = compute_rotation(rotation, sides);
parallel_for(size, [&](const int2 texel) {
const float4 bokeh_value = spectral_bokeh(
texel, size, exterior_angle, corrected_rotation, roundness, catadioptric, lens_shift);
this->result.store_pixel(texel, Color(bokeh_value));
});
}
/* --------------------------------------------------------------------
* Bokeh Kernel Container.
*/
void BokehKernelContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
Result &BokehKernelContainer::get(Context &context,
int2 size,
int sides,
float rotation,
float roundness,
float catadioptric,
float lens_shift)
{
const BokehKernelKey key(size, sides, rotation, roundness, catadioptric, lens_shift);
auto &bokeh_kernel = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<BokehKernel>(
context, size, sides, rotation, roundness, catadioptric, lens_shift);
});
bokeh_kernel.needed = true;
return bokeh_kernel.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,518 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_hash.hh"
#include "BLI_listbase.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_string.h"
#include "BLI_string_ref.hh"
#include "RE_pipeline.h"
#include "GPU_texture.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "IMB_metadata.hh"
#include "BKE_cryptomatte.hh"
#include "BKE_image.hh"
#include "BKE_lib_id.hh"
#include "DNA_ID.h"
#include "DNA_image_types.h"
#include "COM_cached_image.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Cached Image Key.
*/
CachedImageKey::CachedImageKey(const int layer_index,
const std::string pass_name,
const std::string view_name,
const int frame)
: layer_index(layer_index), pass_name(pass_name), view_name(view_name), frame(frame)
{
}
uint64_t CachedImageKey::hash() const
{
return get_default_hash(this->layer_index, this->view_name, this->pass_name, this->frame);
}
bool operator==(const CachedImageKey &a, const CachedImageKey &b)
{
return a.layer_index == b.layer_index && a.pass_name == b.pass_name &&
a.view_name == b.view_name && a.frame == b.frame;
}
/* --------------------------------------------------------------------
* Cached Image.
*/
/* Get the render layer in the given render result specified by the given image user. Returns
* nullptr if not found. */
static RenderLayer *get_render_layer(const RenderResult *render_result,
const ImageUser &image_user)
{
const ListBaseT<RenderLayer> *layers = &render_result->layers;
return static_cast<RenderLayer *>(BLI_findlink(layers, image_user.layer));
}
/* Get the index of the pass with the given name in the render layer specified by the given image
* user in the given render result. Returns -1 if not found. */
static int get_pass_index(const RenderResult *render_result,
const ImageUser &image_user,
const char *name)
{
const RenderLayer *render_layer = get_render_layer(render_result, image_user);
if (!render_layer) {
return -1;
}
return BLI_findstringindex(&render_layer->passes, name, offsetof(RenderPass, name));
}
/* Get the render pass in the given render layer specified by the given image user. */
static RenderPass *get_render_pass(const RenderLayer *render_layer, const ImageUser &image_user)
{
return static_cast<RenderPass *>(BLI_findlink(&render_layer->passes, image_user.pass));
}
/* Get the render pass in the given render result specified by the given image user. */
static RenderPass *get_render_pass(const RenderResult *render_result, const ImageUser &image_user)
{
if (!render_result) {
return nullptr;
}
const RenderLayer *render_layer = get_render_layer(render_result, image_user);
if (!render_layer) {
return nullptr;
}
return get_render_pass(render_layer, image_user);
}
/* Get the index of the view selected in the image user. If the image is not a multi-view image
* or only has a single view, then zero is returned. Otherwise, if the image is a multi-view
* image, the index of the selected view is returned. However, note that the value of the view
* member of the image user is not the actual index of the view. More specifically, the index 0
* is reserved to denote the special mode of operation "All", which dynamically selects the view
* whose name matches the view currently being rendered. It follows that the views are then
* indexed starting from 1. So for non zero view values, the actual index of the view is the
* value of the view member of the image user minus 1. */
static int get_view_index(const Context &context,
const RenderResult *render_result,
const ImageUser &image_user)
{
/* The image is not a multi-view image, so just return zero. */
if (!render_result) {
return 0;
}
const ListBaseT<RenderView> *views = &render_result->views;
/* There is only one view and its index is 0. */
if (BLI_listbase_count_at_most(views, 2) < 2) {
return 0;
}
const int view = image_user.view;
/* The view is not zero, which means it is manually specified and the actual index is then the
* view value minus 1. */
if (view != 0) {
return view - 1;
}
/* Otherwise, the view value is zero, denoting the special mode of operation "All", which finds
* the index of the view whose name matches the view currently being rendered. */
const char *view_name = context.get_view_name().data();
const int matched_view = BLI_findstringindex(views, view_name, offsetof(RenderView, name));
/* No view matches the view currently being rendered, so fallback to the first view. */
if (matched_view == -1) {
return 0;
}
return matched_view;
}
/* Get a copy of the image user that is appropriate to retrieve the needed image buffer from the
* image. This essentially sets the appropriate frame, pass, and view that corresponds to the
* given context and pass name. If the image is a multi-layer image, then the render_result
* argument should be set, otherwise, it is ignored. The image user will have a pass index of -1 if
* the pass/later were not found in the image for multi-layer images. */
static ImageUser compute_image_user_for_pass(const Context &context,
const Image &image,
const RenderResult *render_result,
const ImageUser &image_user,
const char *pass_name)
{
ImageUser image_user_for_pass = image_user;
/* Set the needed view. */
image_user_for_pass.view = get_view_index(context, render_result, image_user_for_pass);
/* Set the needed pass. */
if (BKE_image_is_multilayer(&image)) {
image_user_for_pass.pass = get_pass_index(render_result, image_user_for_pass, pass_name);
BKE_image_multilayer_index(const_cast<RenderResult *>(render_result), &image_user_for_pass);
}
else {
BKE_image_multiview_index(&image, &image_user_for_pass);
}
return image_user_for_pass;
}
/* The image buffer might be stored as an sRGB 8-bit image, while the compositor expects linear
* float images, so compute a linear float buffer for the image buffer. This will also do linear
* space conversion and alpha pre-multiplication as needed. We could store those images in sRGB GPU
* textures and let the GPU do the linear space conversion, but the issues is that we don't control
* how the GPU does the conversion and so we get tiny differences across CPU and GPU compositing,
* and potentially even across GPUs/Drivers. Further, if alpha pre-multiplication is needed, we
* would need to do it ourself, which means alpha pre-multiplication will happen before linear
* space conversion, which would produce yet another difference. So we just do everything on the
* CPU, since this is already a cached resource.
*
* To avoid conflicts with other threads, create a new image buffer and assign all the necessary
* information to it, with IB_DO_NOT_TAKE_OWNERSHIP for buffers since a deep copy is not needed.
*
* The caller should free the returned image buffer. */
static ImBuf *compute_linear_buffer(ImBuf *image_buffer)
{
/* Do not pass the flags to the allocation function to avoid buffer allocation, but assign them
* after to retain important information like precision and alpha mode. */
ImBuf *linear_image_buffer = IMB_allocImBuf(image_buffer->x, image_buffer->y, ImBufFlags::Zero);
linear_image_buffer->color_mode = image_buffer->color_mode;
linear_image_buffer->flags = image_buffer->flags;
/* Assign the float buffer if it exists, as well as its number of channels. */
linear_image_buffer->float_buffer = image_buffer->float_buffer;
linear_image_buffer->channels = image_buffer->channels;
/* If no float buffer exists, assign it then compute a float buffer from it. This is the main
* call of this function. */
if (!linear_image_buffer->float_data()) {
linear_image_buffer->byte_buffer = image_buffer->byte_buffer;
IMB_float_from_byte(linear_image_buffer);
}
/* If the image buffer contained compressed data, assign them as well, but only if the color
* space of the buffer is linear or data, since we need linear data and can't preprocess the
* compressed buffer. If not, we fallback to the float buffer already assigned, which is
* guaranteed to exist as a fallback for compressed textures. */
const bool is_suitable_compressed_color_space =
IMB_colormanagement_space_is_data(image_buffer->byte_buffer.colorspace) ||
IMB_colormanagement_space_is_scene_linear(image_buffer->byte_buffer.colorspace);
if (image_buffer->ftype == IMB_FTYPE_DDS && is_suitable_compressed_color_space) {
linear_image_buffer->ftype = IMB_FTYPE_DDS;
linear_image_buffer->filepath = image_buffer->filepath;
}
return linear_image_buffer;
}
/* Returns the float type of a result given the channels count. */
static ResultType float_type(const int channels_count)
{
switch (channels_count) {
case 1:
return ResultType::Float;
case 2:
return ResultType::Float2;
case 3:
return ResultType::Float3;
case 4:
return ResultType::Color;
default:
break;
}
BLI_assert_unreachable();
return ResultType::Color;
}
/* Returns the appropriate result type for the given render pass. The type is determined based on
* the channels count of the buffer for simple images, while channel IDs are also considered for
* multi-layer images since 3-channel passes can be RGB without alpha and 4-channel passes can be
* XYZW 4D vectors. */
static ResultType get_pass_type(const RenderPass *render_pass)
{
switch (render_pass->channels) {
case 1:
return ResultType::Float;
case 2:
return ResultType::Float2;
case 3:
if (STR_ELEM(render_pass->chan_id, "RGB", "rgb")) {
return ResultType::Color;
}
else {
return ResultType::Float3;
}
case 4:
if (STR_ELEM(render_pass->chan_id, "RGBA", "rgba")) {
return ResultType::Color;
}
else {
return ResultType::Float4;
}
default:
break;
}
BLI_assert_unreachable();
return ResultType::Float;
}
CachedImage::CachedImage(Context &context,
Image &image,
ImageUser &image_user,
const char *pass_name)
: result(context)
{
/* We can't retrieve the needed image buffer yet, because we still need to assign the pass index
* to the image user in order to acquire the image buffer corresponding to the given pass name.
* However, in order to compute the pass index, we need the render result structure of the image
* to be initialized. So we first acquire a dummy image buffer since it initializes the image
* render result as a side effect. We also use that as a mean of validation, since we can early
* exit if the returned image buffer is nullptr. This image buffer can be immediately released.
* Since it carries no important information. */
ImBuf *initial_image_buffer = BKE_image_acquire_ibuf(&image, &image_user, nullptr);
BKE_image_release_ibuf(&image, initial_image_buffer, nullptr);
if (!initial_image_buffer) {
return;
}
RenderResult *render_result = BKE_image_acquire_renderresult(nullptr, &image);
ImageUser image_user_for_pass = compute_image_user_for_pass(
context, image, render_result, image_user, pass_name);
/* Pass or layer were not found. */
if (BKE_image_is_multilayer(&image) && image_user_for_pass.pass == -1) {
BKE_image_release_renderresult(nullptr, &image, render_result);
return;
}
if (BKE_image_is_multilayer(&image)) {
const RenderPass *render_pass = get_render_pass(render_result, image_user_for_pass);
this->result.set_type(get_pass_type(render_pass));
}
this->populate_cryptomatte_meta_data(render_result, image_user_for_pass);
BKE_image_release_renderresult(nullptr, &image, render_result);
ImBuf *image_buffer = BKE_image_acquire_ibuf(&image, &image_user_for_pass, nullptr);
ImBuf *linear_image_buffer = compute_linear_buffer(image_buffer);
this->populate_meta_data(image_buffer);
const bool use_half_float = linear_image_buffer->foptions.flag & OPENEXR_HALF;
this->result.set_precision(use_half_float ? ResultPrecision::Half : ResultPrecision::Full);
if (!BKE_image_is_multilayer(&image)) {
this->result.set_type(float_type(linear_image_buffer->channels));
}
/* For GPU, we wrap the texture returned by IMB module and free it ourselves in destructor. For
* CPU, we allocate the result and copy to it from the image buffer. */
if (context.use_gpu()) {
texture_ = IMB_create_gpu_texture("Image Texture", linear_image_buffer, true, true, false);
GPU_texture_update_mipmap_chain(texture_);
this->result.share_data(texture_);
}
else {
const int2 size = int2(image_buffer->x, image_buffer->y);
Result buffer_result(context, float_type(image_buffer->channels), ResultPrecision::Full);
buffer_result.share_data(linear_image_buffer->float_buffer.data,
size,
linear_image_buffer->float_buffer.sharing_info);
this->result.allocate_texture(size, false);
if (buffer_result.type() == ResultType::Color && result.type() == ResultType::Float4) {
parallel_for(size, [&](const int2 texel) {
this->result.store_pixel(texel, float4(buffer_result.load_pixel<Color>(texel)));
});
}
else if (buffer_result.type() == ResultType::Float3 && result.type() == ResultType::Color) {
/* Color passes with no alpha could be stored in a Float3 type. */
parallel_for(size, [&](const int2 texel) {
this->result.store_pixel(texel,
Color(float4(buffer_result.load_pixel<float3>(texel), 1.0f)));
});
}
else {
result.get_cpp_type().to_static_type<float, float2, float3, float4, Color>(
[&]<typename T>() {
parallel_for(result.domain().data_size, [&](const int2 texel) {
result.store_pixel(texel, buffer_result.load_pixel<T>(texel));
});
});
}
buffer_result.release();
}
if (flag_is_set(image_buffer->flags, ImBufFlags::HasDisplayWindow)) {
this->result.domain().display_size = int2(image_buffer->display_size);
this->result.domain().data_offset = int2(image_buffer->data_offset);
this->result.transform(
math::from_location<float3x3>(float2(int2(image_buffer->display_offset))));
}
IMB_freeImBuf(linear_image_buffer);
BKE_image_release_ibuf(&image, image_buffer, nullptr);
}
void CachedImage::populate_cryptomatte_meta_data(const RenderResult *render_result,
const ImageUser &image_user)
{
if (!render_result) {
return;
}
const RenderLayer *render_layer = get_render_layer(render_result, image_user);
if (!render_layer) {
return;
}
const RenderPass *render_pass = get_render_pass(render_layer, image_user);
if (!render_pass) {
return;
}
/* We assume the given pass is a Cryptomatte pass and retrieve its full name. If it wasn't a
* Cryptomatte pass, the checks below will fail anyways. */
const bool is_named_layer = render_layer->name[0] != '\0';
const std::string layer_prefix = is_named_layer ? std::string(render_layer->name) + "." : "";
const std::string combined_pass_name = layer_prefix + render_pass->name;
StringRef cryptomatte_layer_name = bke::cryptomatte::BKE_cryptomatte_extract_layer_name(
combined_pass_name);
struct StampCallbackData {
std::string cryptomatte_layer_name;
compositor::MetaData *meta_data;
};
/* Go over the stamp data and add any Cryptomatte related meta data. */
StampCallbackData callback_data = {cryptomatte_layer_name, &this->result.meta_data};
BKE_stamp_info_callback(
&callback_data,
render_result->stamp_data,
[](void *user_data, const char *key, char *value, int /*value_length*/) {
StampCallbackData *data = static_cast<StampCallbackData *>(user_data);
const std::string manifest_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "manifest");
if (key == manifest_key) {
data->meta_data->cryptomatte.manifest = value;
}
const std::string hash_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "hash");
if (key == hash_key) {
data->meta_data->cryptomatte.hash = value;
}
const std::string conversion_key = bke::cryptomatte::BKE_cryptomatte_meta_data_key(
data->cryptomatte_layer_name, "conversion");
if (key == conversion_key) {
data->meta_data->cryptomatte.conversion = value;
}
},
false);
}
void CachedImage::populate_meta_data(const ImBuf *image_buffer)
{
IMB_metadata_foreach(
image_buffer,
[](const char *key, const char *value, void *user_data) {
compositor::MetaData *meta_data = static_cast<compositor::MetaData *>(user_data);
meta_data->fields.add(key, value);
},
&this->result.meta_data);
}
CachedImage::~CachedImage()
{
this->result.release();
GPU_TEXTURE_FREE_SAFE(texture_);
}
/* --------------------------------------------------------------------
* Cached Image Container.
*/
void CachedImageContainer::reset()
{
/* First, delete all cached images that are no longer needed. */
for (auto &cached_images_for_id : map_.values()) {
cached_images_for_id.remove_if([](auto item) { return !item.value->needed; });
}
map_.remove_if([](auto item) { return item.value.is_empty(); });
update_counts_.remove_if([&](auto item) { return !map_.contains(item.key); });
/* Second, reset the needed status of the remaining cached images to false to ready them to
* track their needed status for the next evaluation. */
for (auto &cached_images_for_id : map_.values()) {
for (auto &value : cached_images_for_id.values()) {
value->needed = false;
}
}
}
Result &CachedImageContainer::get(Context &context,
Image &image,
const ImageUser &image_user,
const char *pass_name)
{
/* Compute the effective frame number of the image if it was animated. */
ImageUser image_user_for_frame = image_user;
BKE_image_user_frame_calc(&image, &image_user_for_frame, context.get_frame_number());
/* A view of 0 is a special value that means the current view being rendered so use the context
* view name. For other values, just convert the view index into a string and use it as the name,
* while this is not correct it works as the cache key and is very fast compared to reading the
* views from file and finding out their name. */
const std::string view_name = image_user.view == 0 ? std::string(context.get_view_name()) :
std::to_string(image_user.view);
const CachedImageKey key(image_user.layer, pass_name, view_name, image_user_for_frame.framenr);
const std::string library_key = image.id.lib ? image.id.lib->id.name : "";
const std::string id_key = std::string(image.id.name) + library_key;
auto &cached_images_for_id = map_.lookup_or_add_default(id_key);
/* Invalidate the cache for that image if it was changed since it was cached. */
if (!cached_images_for_id.is_empty() &&
image.runtime->update_count != update_counts_.lookup(id_key))
{
cached_images_for_id.clear();
}
auto &cached_image = *cached_images_for_id.lookup_or_add_cb(key, [&]() {
return std::make_unique<CachedImage>(context, image, image_user_for_frame, pass_name);
});
/* Store the current update count to later compare to and check if the image changed. */
update_counts_.add_overwrite(id_key, image.runtime->update_count);
cached_image.needed = true;
return cached_image.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,233 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include "BLI_math_color.h"
#include "BLI_math_vector_types.hh"
#include "BKE_lib_id.hh"
#include "BKE_mask.hh"
#include "DNA_ID.h"
#include "DNA_mask_types.h"
#include "COM_cached_mask.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Cached Mask Key.
*/
CachedMaskKey::CachedMaskKey(const Domain &domain,
float aspect_ratio,
bool use_feather,
bool srgb_to_linear,
int frame,
int motion_blur_samples,
float motion_blur_shutter)
: data_size(domain.data_size),
display_size(domain.display_size),
data_offset(domain.data_offset),
aspect_ratio(aspect_ratio),
use_feather(use_feather),
srgb_to_linear(srgb_to_linear),
frame(frame),
motion_blur_samples(motion_blur_samples),
motion_blur_shutter(motion_blur_shutter)
{
}
uint64_t CachedMaskKey::hash() const
{
return get_default_hash(
get_default_hash(data_size, display_size, data_offset, aspect_ratio, use_feather),
get_default_hash(srgb_to_linear, frame, motion_blur_samples, motion_blur_shutter));
}
/* --------------------------------------------------------------------
* Cached Mask.
*/
static Vector<MaskRasterHandle *> get_mask_raster_handles(Mask *mask,
int2 size,
int frame,
bool frame_is_current,
bool use_feather,
int motion_blur_samples,
float motion_blur_shutter)
{
Vector<MaskRasterHandle *> handles;
if (!mask) {
return handles;
}
/* If motion blur samples are 1 (no motion blur) and frame is current, we can just use currently
* evaluated mask. */
if (motion_blur_samples == 1 && frame_is_current) {
MaskRasterHandle *handle = BKE_maskrasterize_handle_new();
BKE_maskrasterize_handle_init(handle, mask, size.x, size.y, true, true, use_feather);
handles.append(handle);
return handles;
}
/* Otherwise, we have a number of motion blur samples or non-current frame, so make a copy of the
* Mask ID and evaluate it at the needed frames to get the needed raster handles. */
Mask *evaluation_mask = reinterpret_cast<Mask *>(
BKE_id_copy_ex(nullptr, &mask->id, nullptr, LIB_ID_COPY_LOCALIZE | LIB_ID_COPY_NO_ANIMDATA));
/* We evaluate at the frames in the range [current_frame - shutter, current_frame + shutter]. */
const float start_frame = frame - motion_blur_shutter;
const float frame_step = (motion_blur_shutter * 2.0f) / motion_blur_samples;
for (int i = 0; i < motion_blur_samples; i++) {
MaskRasterHandle *handle = BKE_maskrasterize_handle_new();
BKE_mask_evaluate(evaluation_mask, start_frame + frame_step * i, true);
BKE_maskrasterize_handle_init(
handle, evaluation_mask, size.x, size.y, true, true, use_feather);
handles.append(handle);
}
BKE_id_free(nullptr, &evaluation_mask->id);
return handles;
}
CachedMask::CachedMask(Context &context,
Mask *mask,
const Domain &domain,
int frame,
float aspect_ratio,
bool use_feather,
int motion_blur_samples,
float motion_blur_shutter,
bool srgb_to_linear)
: result(context.create_result(ResultType::Float))
{
const bool frame_is_current = context.get_frame_number() == frame;
Vector<MaskRasterHandle *> handles = get_mask_raster_handles(mask,
domain.display_size,
frame,
frame_is_current,
use_feather,
motion_blur_samples,
motion_blur_shutter);
Result result_cpu = context.create_result(ResultType::Float);
result_cpu.allocate_texture(domain, false, ResultStorageType::CPU);
parallel_for(domain.data_size, [&](const int2 texel) {
/* Compute the coordinates in the [0, 1] range and add 0.5 to evaluate the mask at the
* center of pixels. */
float2 coordinates = (float2(texel + domain.data_offset) + 0.5f) / float2(domain.display_size);
/* Do aspect ratio correction around the center 0.5 point. */
coordinates = (coordinates - float2(0.5)) * float2(1.0, aspect_ratio) + float2(0.5);
float mask_value = 0.0f;
for (MaskRasterHandle *handle : handles) {
mask_value += BKE_maskrasterize_handle_sample(handle, coordinates);
}
mask_value /= handles.size();
if (srgb_to_linear) {
mask_value = srgb_to_linearrgb(mask_value);
}
result_cpu.store_pixel(texel, mask_value);
});
for (MaskRasterHandle *handle : handles) {
BKE_maskrasterize_handle_free(handle);
}
if (context.use_gpu()) {
Result result_gpu = result_cpu.upload_to_gpu(false);
this->result.share_data(result_gpu);
result_gpu.release();
}
else {
this->result.share_data(result_cpu);
}
result_cpu.release();
}
CachedMask::~CachedMask()
{
this->result.release();
}
/* --------------------------------------------------------------------
* Cached Mask Container.
*/
void CachedMaskContainer::reset()
{
/* First, delete all cached masks that are no longer needed. */
for (auto &cached_masks_for_id : map_.values()) {
cached_masks_for_id.remove_if([](auto item) { return !item.value->needed; });
}
map_.remove_if([](auto item) { return item.value.is_empty(); });
update_counts_.remove_if([&](auto item) { return !map_.contains(item.key); });
/* Second, reset the needed status of the remaining cached masks to false to ready them to track
* their needed status for the next evaluation. */
for (auto &cached_masks_for_id : map_.values()) {
for (auto &value : cached_masks_for_id.values()) {
value->needed = false;
}
}
}
Result &CachedMaskContainer::get(Context &context,
Mask *mask,
const Domain &domain,
float aspect_ratio,
bool use_feather,
int frame,
int motion_blur_samples,
float motion_blur_shutter,
bool srgb_to_linear)
{
const CachedMaskKey key(domain,
aspect_ratio,
use_feather,
srgb_to_linear,
frame,
motion_blur_samples,
motion_blur_shutter);
const std::string library_key = mask->id.lib ? mask->id.lib->id.name : "";
const std::string id_key = std::string(mask->id.name) + library_key;
auto &cached_masks_for_id = map_.lookup_or_add_default(id_key);
/* Invalidate the cache for that mask if it was changed since it was cached. */
if (!cached_masks_for_id.is_empty() &&
mask->runtime.last_update != update_counts_.lookup(id_key))
{
cached_masks_for_id.clear();
}
auto &cached_mask = *cached_masks_for_id.lookup_or_add_cb(key, [&]() {
return std::make_unique<CachedMask>(context,
mask,
domain,
frame,
aspect_ratio,
use_feather,
motion_blur_samples,
motion_blur_shutter,
srgb_to_linear);
});
/* Store the current update count to later compare to and check if the mask changed. */
update_counts_.add_overwrite(id_key, mask->runtime.last_update);
cached_mask.needed = true;
return cached_mask.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,100 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_hash.hh"
#include "GPU_shader.hh"
#include "COM_cached_shader.hh"
#include "COM_result.hh"
#include "gpu_shader_create_info.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Cached Shader Key.
*/
CachedShaderKey::CachedShaderKey(const char *info_name, ResultPrecision precision)
: info_name(info_name), precision(precision)
{
}
uint64_t CachedShaderKey::hash() const
{
return get_default_hash(info_name, precision);
}
bool operator==(const CachedShaderKey &a, const CachedShaderKey &b)
{
return a.info_name == b.info_name && a.precision == b.precision;
}
/* --------------------------------------------------------------------
* Cached Shader.
*/
CachedShader::CachedShader(const char *info_name, ResultPrecision precision)
{
using namespace gpu::shader;
ShaderCreateInfo info = *reinterpret_cast<const ShaderCreateInfo *>(
GPU_shader_create_info_get(info_name));
/* Finalize first in case the create info had additional info. */
info.finalize();
/* Change the format of image resource to the target precision. */
for (ShaderCreateInfo::Resource &resource : info.pass_resources_) {
if (resource.bind_type != ShaderCreateInfo::Resource::BindType::IMAGE) {
continue;
}
resource.image.format = Result::gpu_texture_format(resource.image.format, precision);
}
shader_ = GPU_shader_create_from_info(reinterpret_cast<const GPUShaderCreateInfo *>(&info));
}
CachedShader::~CachedShader()
{
GPU_shader_free(shader_);
}
gpu::Shader *CachedShader::shader() const
{
return shader_;
}
/* --------------------------------------------------------------------
* Cached Shader Container.
*/
void CachedShaderContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
gpu::Shader *CachedShaderContainer::get(const char *info_name, ResultPrecision precision)
{
const CachedShaderKey key(info_name, precision);
auto &cached_shader = *map_.lookup_or_add_cb(
key, [&]() { return std::make_unique<CachedShader>(info_name, precision); });
cached_shader.needed = true;
return cached_shader.shader();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,327 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* -------------------------------------------------------------------------------------------------
* Deriche Gaussian Coefficients.
*
* Computes the coefficients of the fourth order IIR filter approximating a Gaussian filter
* computed using Deriche's design method. This is based on the following paper:
*
* Deriche, Rachid. Recursively implementating the Gaussian and its derivatives. Diss. INRIA,
* 1993.
*
* But with corrections in the normalization scale from the following paper, as will be seen in the
* implementation:
*
* Farneback, Gunnar, and Carl-Fredrik Westin. Improving Deriche-style recursive Gaussian
* filters. Journal of Mathematical Imaging and Vision 26.3 (2006): 293-299.
*
* The Deriche filter is computed as the sum of a causal and a non causal sequence of second order
* difference equations as can be seen in Equation (30) in Deriche's paper, and the target of this
* class is to compute the feedback, causal feedforward, and non causal feedforward coefficients of
* the filter. */
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "COM_context.hh"
#include "COM_deriche_gaussian_coefficients.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Deriche Gaussian Coefficients Key.
*/
DericheGaussianCoefficientsKey::DericheGaussianCoefficientsKey(float sigma) : sigma(sigma) {}
uint64_t DericheGaussianCoefficientsKey::hash() const
{
return get_default_hash(sigma);
}
bool operator==(const DericheGaussianCoefficientsKey &a, const DericheGaussianCoefficientsKey &b)
{
return a.sigma == b.sigma;
}
/* -------------------------------------------------------------------------------------------------
* Deriche Gaussian Coefficients.
*/
/* The base constant coefficients computed using Deriche's method with 10 digits of precision.
* Those are available in Deriche's paper by comparing Equations (19) and (38). */
inline constexpr static double a0 = 1.6797292232361107;
inline constexpr static double a1 = 3.7348298269103580;
inline constexpr static double b0 = 1.7831906544515104;
inline constexpr static double b1 = 1.7228297663338028;
inline constexpr static double c0 = -0.6802783501806897;
inline constexpr static double c1 = -0.2598300478959625;
inline constexpr static double w0 = 0.6318113174569493;
inline constexpr static double w1 = 1.9969276832487770;
/* Computes n00 in Equation (21) in Deriche's paper. */
static double compute_numerator_0()
{
return a0 + c0;
}
/* Computes n11 in Equation (21) in Deriche's paper. */
static double compute_numerator_1(float sigma)
{
const double multiplier1 = math::exp(-b1 / sigma);
const double term1 = c1 * math::sin(w1 / sigma) - (c0 + 2.0 * a0) * math::cos(w1 / sigma);
const double multiplier2 = math::exp(-b0 / sigma);
const double term2 = a1 * math::sin(w0 / sigma) - (2.0 * c0 + a0) * math::cos(w0 / sigma);
return multiplier1 * term1 + multiplier2 * term2;
}
/* Computes n22 in Equation (21) in Deriche's paper. */
static double compute_numerator_2(float sigma)
{
const double multiplier1 = 2.0 * math::exp(-(b0 / sigma) - (b1 / sigma));
const double term11 = (a0 + c0) * math::cos(w1 / sigma) * math::cos(w0 / sigma);
const double term12 = math::cos(w1 / sigma) * a1 * math::sin(w0 / sigma);
const double term13 = math::cos(w0 / sigma) * c1 * math::sin(w1 / sigma);
const double term1 = term11 - term12 - term13;
const double term2 = c0 * math::exp(-2.0 * (b0 / sigma));
const double term3 = a0 * math::exp(-2.0 * (b1 / sigma));
return multiplier1 * term1 + term2 + term3;
}
/* Computes n33 in Equation (21) in Deriche's paper. */
static double compute_numerator_3(float sigma)
{
const double multiplier1 = math::exp(-(b1 / sigma) - 2.0 * (b0 / sigma));
const double term1 = c1 * math::sin(w1 / sigma) - math::cos(w1 / sigma) * c0;
const double multiplier2 = math::exp(-(b0 / sigma) - 2.0 * (b1 / sigma));
const double term2 = a1 * math::sin(w0 / sigma) - math::cos(w0 / sigma) * a0;
return multiplier1 * term1 + multiplier2 * term2;
}
/* Computes and packs the numerators in Equation (21) in Deriche's paper. */
static double4 compute_numerator(float sigma)
{
const double n0 = compute_numerator_0();
const double n1 = compute_numerator_1(sigma);
const double n2 = compute_numerator_2(sigma);
const double n3 = compute_numerator_3(sigma);
return double4(n0, n1, n2, n3);
}
/* Computes d11 in Equation (22) in Deriche's paper. */
static double compute_denominator_1(float sigma)
{
const double term1 = -2.0 * math::exp(-(b0 / sigma)) * math::cos(w0 / sigma);
const double term2 = 2.0 * math::exp(-(b1 / sigma)) * math::cos(w1 / sigma);
return term1 - term2;
}
/* Computes d22 in Equation (22) in Deriche's paper. */
static double compute_denominator_2(float sigma)
{
const double term1 = 4.0 * math::cos(w1 / sigma) * math::cos(w0 / sigma);
const double multiplier1 = math::exp(-(b0 / sigma) - (b1 / sigma));
const double term2 = math::exp(-2.0 * (b1 / sigma));
const double term3 = math::exp(-2.0 * (b0 / sigma));
return term1 * multiplier1 + term2 + term3;
}
/* Computes d33 in Equation (22) in Deriche's paper. */
static double compute_denominator_3(float sigma)
{
const double term1 = -2.0 * math::cos(w0 / sigma);
const double multiplier1 = math::exp(-(b0 / sigma) - 2.0 * (b1 / sigma));
const double term2 = 2.0 * math::cos(w1 / sigma);
const double multiplier2 = math::exp(-(b1 / sigma) - 2.0 * (b0 / sigma));
return term1 * multiplier1 - term2 * multiplier2;
}
/* Computes d44 in Equation (22) in Deriche's paper. */
static double compute_denominator_4(float sigma)
{
return math::exp(-2.0 * (b0 / sigma) - 2.0 * (b1 / sigma));
}
/* Computes and packs the denominators in Equation (22) in Deriche's paper. */
static double4 compute_denominator(float sigma)
{
const double d1 = compute_denominator_1(sigma);
const double d2 = compute_denominator_2(sigma);
const double d3 = compute_denominator_3(sigma);
const double d4 = compute_denominator_4(sigma);
return double4(d1, d2, d3, d4);
}
/* Computes the normalization scale that the feedforward coefficients should be divided by to
* match the unit integral of the Gaussian. The scaling factor proposed by Deriche's paper in
* Equation (50) is wrong due to missing terms. A correct scaling factor is presented in
* Farneback's paper in Equation (25), which is implemented in this method. */
static float compute_normalization_scale(const double4 &causal_feedforward_coefficients,
const double4 &feedback_coefficients)
{
const double causal_feedforwad_sum = math::reduce_add(causal_feedforward_coefficients);
const double feedback_sum = 1.0 + math::reduce_add(feedback_coefficients);
return 2.0 * (causal_feedforwad_sum / feedback_sum) - causal_feedforward_coefficients[0];
}
/* Computes the non causal feedforward coefficients from the feedback and causal feedforward
* coefficients based on Equation (31) in Deriche's paper. Notice that the equation is linear, so
* the coefficients can be computed after the normalization of the causal feedforward
* coefficients. */
static double4 compute_non_causal_feedforward_coefficients(
const double4 &causal_feedforward_coefficients, const double4 &feedback_coefficients)
{
const double n1 = causal_feedforward_coefficients[1] -
feedback_coefficients[0] * causal_feedforward_coefficients[0];
const double n2 = causal_feedforward_coefficients[2] -
feedback_coefficients[1] * causal_feedforward_coefficients[0];
const double n3 = causal_feedforward_coefficients[3] -
feedback_coefficients[2] * causal_feedforward_coefficients[0];
const double n4 = -feedback_coefficients[3] * causal_feedforward_coefficients[0];
return double4(n1, n2, n3, n4);
}
/**
* The IIR filter difference equation relies on previous outputs to compute new outputs, those
* previous outputs are not really defined at the start of the filter. To do Neumann boundary
* condition, we initialize the previous output with a special value that is a function of the
* boundary value. This special value is computed by multiply the boundary value with a coefficient
* to simulate an infinite stream of the boundary value.
*
* The function for the coefficient can be derived by substituting the boundary value for previous
* inputs, equating all current and previous outputs to the same value, and finally rearranging to
* compute that same output value.
*
* Start by the difference equation where b_i are the feedforward coefficients and a_i are the
* feedback coefficients:
*
* \code{.tex}
* y[n] = \sum_{i = 0}^3 b_i x[n - i] - \sum_{i = 0}^3 a_i y[n - i]
* \endcode
*
* Assume all outputs are y and all inputs are x, which is the boundary value:
*
* \code{.tex}
* y = \sum_{i = 0}^3 b_i x - \sum_{i = 0}^3 a_i y
* \endcode
*
* Now rearrange to compute y:
*
* \code{.tex}
* y = x \sum_{i = 0}^3 b_i - y \sum_{i = 0}^3 a_i
* y + y \sum_{i = 0}^3 a_i = x \sum_{i = 0}^3 b_i
* y (1 + \sum_{i = 0}^3 a_i) = x \sum_{i = 0}^3 b_i
* y = x \cdot \frac{\sum_{i = 0}^3 b_i}{1 + \sum_{i = 0}^3 a_i}
* \endcode
*
* So our coefficient is the value that is multiplied by the boundary value x. Had x been zero,
* that is, we are doing Dirichlet boundary condition, the equations still hold.
*/
static double compute_boundary_coefficient(const double4 &feedforward_coefficients,
const double4 &feedback_coefficients)
{
return math::reduce_add(feedforward_coefficients) /
(1.0 + math::reduce_add(feedback_coefficients));
}
/* Computes the feedback, causal feedforward, and non causal feedforward coefficients given a
* target Gaussian sigma value as used in Equations (28) and (29) in Deriche's paper. */
DericheGaussianCoefficients::DericheGaussianCoefficients(Context & /*context*/, float sigma)
{
/* The numerator coefficients are the causal feedforward coefficients and the denominator
* coefficients are the feedback coefficients as can be seen in Equation (28). */
causal_feedforward_coefficients_ = compute_numerator(sigma);
feedback_coefficients_ = compute_denominator(sigma);
/* Normalize the feedforward coefficients as discussed in Section "5.4 Normalization" in
* Deriche's paper. Feedback coefficients do not need normalization. */
causal_feedforward_coefficients_ /= compute_normalization_scale(causal_feedforward_coefficients_,
feedback_coefficients_);
/* Compute the non causal feedforward coefficients from the feedback and normalized causal
* feedforward coefficients based on Equation (31) from Deriche's paper. Since the causal
* coefficients are already normalized, this doesn't need normalization. */
non_causal_feedforward_coefficients_ = compute_non_causal_feedforward_coefficients(
causal_feedforward_coefficients_, feedback_coefficients_);
/* Compute the boundary coefficient for both the causal and non causal filters. */
causal_boundary_coefficient_ = compute_boundary_coefficient(causal_feedforward_coefficients_,
feedback_coefficients_);
non_causal_boundary_coefficient_ = compute_boundary_coefficient(
non_causal_feedforward_coefficients_, feedback_coefficients_);
}
const double4 &DericheGaussianCoefficients::feedback_coefficients() const
{
return feedback_coefficients_;
}
const double4 &DericheGaussianCoefficients::causal_feedforward_coefficients() const
{
return causal_feedforward_coefficients_;
}
const double4 &DericheGaussianCoefficients::non_causal_feedforward_coefficients() const
{
return non_causal_feedforward_coefficients_;
}
double DericheGaussianCoefficients::causal_boundary_coefficient() const
{
return causal_boundary_coefficient_;
}
double DericheGaussianCoefficients::non_causal_boundary_coefficient() const
{
return non_causal_boundary_coefficient_;
}
/* --------------------------------------------------------------------
* Deriche Gaussian Coefficients Container.
*/
void DericheGaussianCoefficientsContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
DericheGaussianCoefficients &DericheGaussianCoefficientsContainer::get(Context &context,
float sigma)
{
const DericheGaussianCoefficientsKey key(sigma);
auto &deriche_gaussian_coefficients = *map_.lookup_or_add_cb(
key, [&]() { return std::make_unique<DericheGaussianCoefficients>(context, sigma); });
deriche_gaussian_coefficients.needed = true;
return deriche_gaussian_coefficients;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,283 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_movieclip_types.h"
#include "DNA_tracking_types.h"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "COM_context.hh"
#include "COM_distortion_grid.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Distortion Grid Key.
*/
DistortionGridKey::DistortionGridKey(const MovieTrackingCamera &camera,
Domain domain,
DistortionType type,
int2 calibration_size)
: camera(camera), domain(domain), type(type), calibration_size(calibration_size)
{
}
uint64_t DistortionGridKey::hash() const
{
return get_default_hash(BKE_tracking_camera_distortion_hash(&camera),
domain.data_size,
domain.display_size,
domain.data_offset,
type,
calibration_size);
}
bool operator==(const DistortionGridKey &a, const DistortionGridKey &b)
{
return BKE_tracking_camera_distortion_equal(&a.camera, &b.camera) &&
a.domain.data_size == b.domain.data_size &&
a.domain.display_size == b.domain.display_size &&
a.domain.data_offset == b.domain.data_offset && a.type == b.type &&
a.calibration_size == b.calibration_size;
}
/* --------------------------------------------------------------------
* Distortion Grid.
*/
/* Reduces the given function in parallel over the given range, the reduction function should have
* the given identity value. The given function gets as arguments the index of the element of the
* range as well as a reference to the value where the result should be accumulated, while the
* reduction function gets a reference to two values and returns their reduction. */
template<typename Value, typename Function, typename Reduction>
static Value parallel_reduce(const int range,
const Value &identity,
const Function &function,
const Reduction &reduction)
{
return threading::parallel_reduce(
IndexRange(range),
32,
identity,
[&](const IndexRange sub_range, const Value &initial_value) {
Value result = initial_value;
for (const int64_t i : sub_range) {
function(i, result);
}
return result;
},
reduction);
}
/* Given the domain of an image, compute its domain after distortion by the given distortion
* parameters. The data window of the domain will likely grow or shrink depending on the
* distortion, while the display window will stay the same. */
static Domain compute_output_domain(MovieDistortion *distortion,
const int2 &calibration_size,
const DistortionType &type,
const Domain &domain)
{
auto distortion_function = [&](const float2 &coordinates) {
/* We are looping over the data space, so transfer to the display space by adding the data
* offset. Finally, transform to the calibration space since this is what the distortion
* functions expect. */
const float2 display_coordinates = coordinates + float2(domain.data_offset);
const float2 normalized_coordinates = display_coordinates / float2(domain.display_size);
const float2 calibrated_coordinates = normalized_coordinates * float2(calibration_size);
float2 distorted_coordinates;
if (type == DistortionType::Undistort) {
BKE_tracking_distortion_undistort_v2(
distortion, calibrated_coordinates, distorted_coordinates);
}
else {
BKE_tracking_distortion_distort_v2(
distortion, calibrated_coordinates, distorted_coordinates);
}
/* Undo the space transformations into the data space and finally into the normalized sampling
* coordinates. */
const float2 distorted_normalized_coordinates = distorted_coordinates /
float2(calibration_size);
const float2 distorted_display_coordinates = distorted_normalized_coordinates *
float2(domain.display_size);
return distorted_display_coordinates;
};
/* Maximum distorted x location along the right edge of the image. */
const float maximum_x = parallel_reduce(
domain.data_size.y + 1,
std::numeric_limits<float>::lowest(),
[&](const int i, float &accumulated_value) {
const float2 position = float2(domain.data_size.x, i);
accumulated_value = math::max(accumulated_value, distortion_function(position).x);
},
math::max<float>);
/* Minimum distorted x location along the left edge of the image. */
const float minimum_x = parallel_reduce(
domain.data_size.y + 1,
std::numeric_limits<float>::max(),
[&](const int i, float &accumulated_value) {
const float2 position = float2(0.0f, i);
accumulated_value = math::min(accumulated_value, distortion_function(position).x);
},
math::min<float>);
/* Minimum distorted y location along the bottom edge of the image. */
const float minimum_y = parallel_reduce(
domain.data_size.x + 1,
std::numeric_limits<float>::max(),
[&](const int i, float &accumulated_value) {
const float2 position = float2(i, 0.0f);
accumulated_value = math::min(accumulated_value, distortion_function(position).y);
},
math::min<float>);
/* Maximum distorted y location along the top edge of the image. */
const float maximum_y = parallel_reduce(
domain.data_size.x + 1,
std::numeric_limits<float>::lowest(),
[&](const int i, float &accumulated_value) {
const float2 position = float2(i, domain.data_size.y);
accumulated_value = math::max(accumulated_value, distortion_function(position).y);
},
math::max<float>);
/* Compute the deltas from the image edges to the maximum/minimum distorted location along the
* direction of that edge. */
const float2 lower_left_delta = float2(0.0f) - float2(minimum_x, minimum_y);
const float2 upper_right_delta = float2(maximum_x, maximum_y) - float2(domain.data_size);
/* Rounds the deltas away from zero and clamp to the size to avoid excessive sizes in case of
* extreme distortion. */
const int2 lower_left_offset = math::min(domain.data_size, int2(math::ceil(lower_left_delta)));
const int2 upper_right_offset = math::min(domain.data_size, int2(math::ceil(upper_right_delta)));
/* Grow/Shrink the domain of the image according to the offsets and set the data offset
* accordingly. */
Domain output_domain = domain;
output_domain.data_size = domain.data_size + lower_left_offset + upper_right_offset;
output_domain.data_offset = -lower_left_offset;
return output_domain;
}
DistortionGrid::DistortionGrid(Context &context,
MovieClip *movie_clip,
Domain domain,
DistortionType type,
int2 calibration_size)
: result(context.create_result(ResultType::Float2, ResultPrecision::Full))
{
MovieDistortion *distortion = BKE_tracking_distortion_new(
&movie_clip->tracking, calibration_size.x, calibration_size.y);
const Domain output_domain = compute_output_domain(distortion, calibration_size, type, domain);
Result distortion_grid_cpu = context.create_result(ResultType::Float2, ResultPrecision::Full);
distortion_grid_cpu.allocate_texture(output_domain, false, ResultStorageType::CPU);
parallel_for(distortion_grid_cpu.domain().data_size, [&](const int2 texel) {
/* We are looping over the data space, so transfer to the display space by adding the data
* offset. Add 0.5 to distort at the pixel centers. Finally, transform to the calibration space
* since this is what the distortion functions expect. */
const float2 display_coordinates = float2(texel + output_domain.data_offset) + 0.5f;
const float2 normalized_coordinates = display_coordinates / float2(domain.display_size);
const float2 calibrated_coordinates = normalized_coordinates * float2(calibration_size);
/* Notice that if we are undistorting the image, we need to distort the coordinates space and
* vice versa, hence the inverted condition. */
float2 distorted_coordinates;
if (type == DistortionType::Undistort) {
BKE_tracking_distortion_distort_v2(
distortion, calibrated_coordinates, distorted_coordinates);
}
else {
BKE_tracking_distortion_undistort_v2(
distortion, calibrated_coordinates, distorted_coordinates);
}
/* Undo the space transformations into the data space and finally into the normalized sampling
* coordinates. */
const float2 distorted_normalized_coordinates = distorted_coordinates /
float2(calibration_size);
const float2 distorted_display_coordinates = distorted_normalized_coordinates *
float2(domain.display_size);
const float2 distorted_data_coordinates = distorted_display_coordinates -
float2(domain.data_offset);
const float2 sampling_coordinates = distorted_data_coordinates / float2(domain.data_size);
distortion_grid_cpu.store_pixel(texel, sampling_coordinates);
});
BKE_tracking_distortion_free(distortion);
if (context.use_gpu()) {
Result distortion_grid_gpu = distortion_grid_cpu.upload_to_gpu(false);
this->result.share_data(distortion_grid_gpu);
distortion_grid_gpu.release();
}
else {
this->result.share_data(distortion_grid_cpu);
}
distortion_grid_cpu.release();
}
DistortionGrid::~DistortionGrid()
{
this->result.release();
}
/* --------------------------------------------------------------------
* Distortion Grid Container.
*/
void DistortionGridContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
static int2 get_movie_clip_size(MovieClip *movie_clip, int frame_number)
{
MovieClipUser user = {};
BKE_movieclip_user_set_frame(&user, frame_number);
int2 size;
BKE_movieclip_get_size(movie_clip, &user, &size.x, &size.y);
return size;
}
Result &DistortionGridContainer::get(
Context &context, MovieClip *movie_clip, Domain domain, DistortionType type, int frame_number)
{
const int2 calibration_size = get_movie_clip_size(movie_clip, frame_number);
const DistortionGridKey key(movie_clip->tracking.camera, domain, type, calibration_size);
auto &distortion_grid = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<DistortionGrid>(context, movie_clip, domain, type, calibration_size);
});
distortion_grid.needed = true;
return distortion_grid.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,184 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <complex>
#include <cstdint>
#include <memory>
#include <numeric>
#if defined(WITH_FFTW3)
# include <fftw3.h>
#endif
#include "BLI_enumerable_thread_specific.hh"
#include "BLI_hash.hh"
#include "BLI_index_range.hh"
#include "BLI_math_base.h"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_task.hh"
#include "COM_fog_glow_kernel.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Fog Glow Kernel Key.
*/
FogGlowKernelKey::FogGlowKernelKey(int kernel_size,
int2 spatial_size,
math::AngleRadian field_of_view)
: kernel_size(kernel_size), spatial_size(spatial_size), field_of_view(field_of_view)
{
}
uint64_t FogGlowKernelKey::hash() const
{
return get_default_hash(kernel_size, spatial_size, field_of_view.degree());
}
bool operator==(const FogGlowKernelKey &a, const FogGlowKernelKey &b)
{
return a.kernel_size == b.kernel_size && a.spatial_size == b.spatial_size &&
a.field_of_view == b.field_of_view;
}
/* --------------------------------------------------------------------
* Fog Glow Kernel.
*/
/* Given the texel coordinates and the constant field-of-view-per-pixel value, under the assumption
* of a relatively small field of view as discussed in Section 3.2, this function computes the
* fog glow kernel value. The kernel value is derived from Equation (5) of the following paper:
*
* Spencer, Greg, et al. "Physically-Based Glare Effects for Digital Images."
* Proceedings of the 22nd Annual Conference on Computer Graphics and Interactive Techniques,
* 1995.
*/
[[maybe_unused]] static float compute_fog_glow_kernel_value(
int2 texel, math::AngleRadian field_of_view_per_pixel)
{
const float theta_degree = math::length(float2(texel)) * field_of_view_per_pixel.degree();
const float f0 = 2.61f * 1e6f * math::exp(-math::square(theta_degree / 0.02f));
const float f1 = 20.91f / math::cube(theta_degree + 0.02f);
const float f2 = 72.37f / math::square(theta_degree + 0.02f);
const float kernel_value = 0.384f * f0 + 0.478f * f1 + 0.138f * f2;
return kernel_value;
}
FogGlowKernel::FogGlowKernel(int kernel_size, int2 spatial_size, math::AngleRadian field_of_view)
{
#if defined(WITH_FFTW3)
/* The FFTW real to complex transforms utilizes the hermitian symmetry of real transforms and
* stores only half the output since the other half is redundant, so we only allocate half of
* the first dimension. See Section 4.3.4 Real-data DFT Array Format in the FFTW manual for
* more information. */
const int2 frequency_size = int2(spatial_size.x / 2 + 1, spatial_size.y);
float *kernel_spatial_domain = fftwf_alloc_real(spatial_size.x * spatial_size.y);
frequencies_ = reinterpret_cast<std::complex<float> *>(
fftwf_alloc_complex(frequency_size.x * frequency_size.y));
/* Create a real to complex plan to transform the kernel to the frequency domain. */
fftwf_plan forward_plan = fftwf_plan_dft_r2c_2d(spatial_size.y,
spatial_size.x,
kernel_spatial_domain,
reinterpret_cast<fftwf_complex *>(frequencies_),
FFTW_ESTIMATE);
/* Use a double to sum the kernel since floats are not stable with threaded summation. */
threading::EnumerableThreadSpecific<double> sum_by_thread([]() { return 0.0; });
/* Compute the entire kernel's spatial space using compute_fog_glow_kernel_value. */
threading::parallel_for(IndexRange(spatial_size.y), 1, [&](const IndexRange sub_y_range) {
double &sum = sum_by_thread.local();
for (const int64_t y : sub_y_range) {
for (const int64_t x : IndexRange(spatial_size.x)) {
const int2 texel = int2(x, y);
const int2 center_texel = spatial_size / 2;
const int2 kernel_texel = texel - center_texel;
const math::AngleRadian field_of_view_per_pixel = field_of_view / kernel_size;
const float kernel_value = compute_fog_glow_kernel_value(kernel_texel,
field_of_view_per_pixel);
sum += kernel_value;
/* We offset the computed kernel with wrap around such that it is centered at the zero
* point, which is the expected format for doing circular convolutions in the frequency
* domain. */
int64_t output_x = mod_i(kernel_texel.x, spatial_size.x);
int64_t output_y = mod_i(kernel_texel.y, spatial_size.y);
kernel_spatial_domain[output_x + output_y * spatial_size.x] = kernel_value;
}
}
});
fftwf_execute_dft_r2c(
forward_plan, kernel_spatial_domain, reinterpret_cast<fftwf_complex *>(frequencies_));
fftwf_destroy_plan(forward_plan);
fftwf_free(kernel_spatial_domain);
/* The computed kernel is not normalized and should be normalized, but instead of normalizing the
* kernel during computation, we normalize it in the frequency domain when convolving the kernel
* to the image since we will be doing sample normalization anyways. This is okay since the
* Fourier transform is linear. */
normalization_factor_ = float(std::accumulate(sum_by_thread.begin(), sum_by_thread.end(), 0.0));
#else
UNUSED_VARS(kernel_size, spatial_size, field_of_view);
#endif
}
FogGlowKernel::~FogGlowKernel()
{
#if defined(WITH_FFTW3)
fftwf_free(frequencies_);
#endif
}
std::complex<float> *FogGlowKernel::frequencies() const
{
return frequencies_;
}
float FogGlowKernel::normalization_factor() const
{
return normalization_factor_;
}
/* --------------------------------------------------------------------
* Fog Glow Kernel Container.
*/
void FogGlowKernelContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
FogGlowKernel &FogGlowKernelContainer::get(int kernel_size,
int2 spatial_size,
math::AngleRadian field_of_view)
{
const FogGlowKernelKey key(kernel_size, spatial_size, field_of_view);
auto &kernel = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<FogGlowKernel>(kernel_size, spatial_size, field_of_view);
});
kernel.needed = true;
return kernel;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,163 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include "BLI_assert.h"
#include "BLI_hash.hh"
#include "BLI_math_vector_types.hh"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_image_coordinates.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Image Coordinates Key.
*/
ImageCoordinatesKey::ImageCoordinatesKey(const Domain &domain, const CoordinatesType type)
: data_size(domain.data_size),
display_size(domain.display_size),
data_offset(domain.data_offset),
type(type)
{
}
uint64_t ImageCoordinatesKey::hash() const
{
return get_default_hash(this->data_size, this->display_size, this->data_offset, this->type);
}
bool operator==(const ImageCoordinatesKey &a, const ImageCoordinatesKey &b)
{
return a.data_size == b.data_size && a.display_size == b.display_size &&
a.data_offset == b.data_offset && a.type == b.type;
}
/* --------------------------------------------------------------------
* Image Coordinates.
*/
ImageCoordinates::ImageCoordinates(Context &context,
const Domain &domain,
const CoordinatesType type)
: result(context.create_result(type == CoordinatesType::Pixel ? ResultType::Int2 :
ResultType::Float2))
{
this->result.allocate_texture(domain, false);
if (context.use_gpu()) {
this->compute_gpu(context, type);
}
else {
this->compute_cpu(type);
}
}
ImageCoordinates::~ImageCoordinates()
{
this->result.release();
}
static const char *get_shader_name(const CoordinatesType type)
{
switch (type) {
case CoordinatesType::Uniform:
return "compositor_image_coordinates_uniform";
case CoordinatesType::Normalized:
return "compositor_image_coordinates_normalized";
case CoordinatesType::Pixel:
return "compositor_image_coordinates_pixel";
}
BLI_assert_unreachable();
return "";
}
void ImageCoordinates::compute_gpu(Context &context, const CoordinatesType type)
{
gpu::Shader *shader = context.get_shader(get_shader_name(type));
GPU_shader_bind(shader);
const Domain domain = this->result.domain();
GPU_shader_uniform_2iv(shader, "data_offset", domain.data_offset);
if (type != CoordinatesType::Pixel) {
GPU_shader_uniform_2iv(shader, "display_size", domain.display_size);
}
this->result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, domain.data_size);
this->result.unbind_as_image();
GPU_shader_unbind();
}
void ImageCoordinates::compute_cpu(const CoordinatesType type)
{
const Domain domain = this->result.domain();
switch (type) {
case CoordinatesType::Uniform: {
const int max_display_size = math::reduce_max(domain.display_size);
parallel_for(domain.data_size, [&](const int2 texel) {
const float2 coordinates = float2(domain.data_offset + texel) + 0.5f;
const float2 centered_coordinates = coordinates - float2(domain.display_size) / 2.0f;
const float2 normalized_coordinates = (centered_coordinates / max_display_size) * 2.0f;
this->result.store_pixel(texel, normalized_coordinates);
});
break;
}
case CoordinatesType::Normalized: {
parallel_for(domain.data_size, [&](const int2 texel) {
const float2 coordinates = float2(domain.data_offset + texel) + 0.5f;
const float2 normalized_coordinates = coordinates / float2(domain.display_size);
this->result.store_pixel(texel, normalized_coordinates);
});
break;
}
case CoordinatesType::Pixel: {
parallel_for(domain.data_size, [&](const int2 texel) {
this->result.store_pixel(texel, domain.data_offset + texel);
});
break;
}
}
}
/* --------------------------------------------------------------------
* Image Coordinates Container.
*/
void ImageCoordinatesContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
Result &ImageCoordinatesContainer::get(Context &context,
const Domain &domain,
const CoordinatesType type)
{
const ImageCoordinatesKey key(domain, type);
auto &pixel_coordinates = *map_.lookup_or_add_cb(
key, [&]() { return std::make_unique<ImageCoordinates>(context, domain, type); });
pixel_coordinates.needed = true;
return pixel_coordinates.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,292 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_hash.hh"
#include "BLI_listbase.h"
#include "BLI_math_base.hh"
#include "BLI_math_color.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_vector.hh"
#include "IMB_imbuf.hh"
#include "DNA_movieclip_types.h"
#include "DNA_tracking_types.h"
#include "GPU_shader.hh"
#include "GPU_storage_buffer.hh"
#include "BKE_movieclip.hh"
#include "BKE_tracking.hh"
#include "COM_context.hh"
#include "COM_keying_screen.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Keying Screen Key.
*/
KeyingScreenKey::KeyingScreenKey(int frame, float smoothness)
: frame(frame), smoothness(smoothness)
{
}
uint64_t KeyingScreenKey::hash() const
{
return get_default_hash(frame, smoothness);
}
bool operator==(const KeyingScreenKey &a, const KeyingScreenKey &b)
{
return a.frame == b.frame && a.smoothness == b.smoothness;
}
/* --------------------------------------------------------------------
* Keying Screen.
*/
/* Computes the color and normalized positions of the keying screen markers in the given movie
* tracking object. The color is computed as the mean color of the search pattern of the marker. */
static void compute_marker_points(MovieClip *movie_clip,
MovieClipUser &movie_clip_user,
MovieTrackingObject *movie_tracking_object,
Vector<float2> &marker_positions,
Vector<float4> &marker_colors)
{
BLI_assert(marker_positions.is_empty());
BLI_assert(marker_colors.is_empty());
ImBuf *image_buffer = BKE_movieclip_get_ibuf(movie_clip, &movie_clip_user);
if (!image_buffer) {
return;
}
for (MovieTrackingTrack &track : movie_tracking_object->tracks) {
const MovieTrackingMarker *marker = BKE_tracking_marker_get(&track, movie_clip_user.framenr);
if (marker->flag & MARKER_DISABLED) {
continue;
}
/* Skip out of bound markers since they have no corresponding color. */
const float2 position = float2(marker->pos) + float2(track.offset);
if (math::clamp(position, float2(0.0f), float2(1.0f)) != position) {
continue;
}
ImBuf *pattern_image_buffer = BKE_tracking_get_pattern_imbuf(
image_buffer, &track, marker, true, false);
if (!pattern_image_buffer) {
continue;
}
/* Find the mean color of the rectangular search pattern of the marker. */
float4 mean_color = float4(0.0f);
const uchar *byte_data = pattern_image_buffer->byte_data();
const float *float_data = pattern_image_buffer->float_data();
for (int i = 0; i < pattern_image_buffer->x * pattern_image_buffer->y; i++) {
if (float_data) {
mean_color += float4(&float_data[i * 4]);
}
else {
float4 linear_color;
uchar4 srgb_color = uchar4(&byte_data[i * 4]);
srgb_to_linearrgb_uchar4(linear_color, srgb_color);
mean_color += linear_color;
}
}
mean_color /= pattern_image_buffer->x * pattern_image_buffer->y;
marker_colors.append(mean_color);
marker_positions.append(position);
IMB_freeImBuf(pattern_image_buffer);
}
IMB_freeImBuf(image_buffer);
}
/* Get a MovieClipUser with an initialized clip frame number. */
static MovieClipUser get_movie_clip_user(Context &context, MovieClip *movie_clip)
{
MovieClipUser movie_clip_user = {};
const int scene_frame = context.get_frame_number();
const int clip_frame = BKE_movieclip_remap_scene_to_clip_frame(movie_clip, scene_frame);
BKE_movieclip_user_set_frame(&movie_clip_user, clip_frame);
return movie_clip_user;
}
KeyingScreen::KeyingScreen(Context &context,
MovieClip *movie_clip,
MovieTrackingObject *movie_tracking_object,
const float smoothness)
: result(context.create_result(ResultType::Color))
{
int2 size;
MovieClipUser movie_clip_user = get_movie_clip_user(context, movie_clip);
BKE_movieclip_get_size(movie_clip, &movie_clip_user, &size.x, &size.y);
Vector<float2> marker_positions;
Vector<float4> marker_colors;
compute_marker_points(
movie_clip, movie_clip_user, movie_tracking_object, marker_positions, marker_colors);
if (marker_positions.is_empty()) {
return;
}
this->result.allocate_texture(Domain(size), false);
if (context.use_gpu()) {
this->compute_gpu(context, smoothness, marker_positions, marker_colors);
}
else {
this->compute_cpu(smoothness, marker_positions, marker_colors);
}
}
void KeyingScreen::compute_gpu(Context &context,
const float smoothness,
Vector<float2> &marker_positions,
const Vector<float4> &marker_colors)
{
gpu::Shader *shader = context.get_shader("compositor_keying_screen");
GPU_shader_bind(shader);
GPU_shader_uniform_1f(shader, "smoothness", smoothness);
GPU_shader_uniform_1i(shader, "number_of_markers", marker_positions.size());
/* SSBO needs to be aligned to 16 bytes, and since sizeof(float2) is only 8 bytes, we need to add
* a dummy element at the end for odd sizes to satisfy the alignment requirement. Notice that the
* number_of_markers uniform was already assigned above to the original size, so the dummy
* element has no effect in the shader. Also notice that the marker colors are always 16 byte
* aligned since sizeof(float4) is 16 bytes, so not need to add anything there. */
if (marker_positions.size() % 2 == 1) {
marker_positions.append(float2(0.0f));
}
gpu::StorageBuf *positions_ssbo = GPU_storagebuf_create_ex(marker_positions.size() *
sizeof(float2),
marker_positions.data(),
GPU_USAGE_STATIC,
"Marker Positions");
const int positions_ssbo_location = GPU_shader_get_ssbo_binding(shader, "marker_positions");
GPU_storagebuf_bind(positions_ssbo, positions_ssbo_location);
gpu::StorageBuf *colors_ssbo = GPU_storagebuf_create_ex(marker_colors.size() * sizeof(float4),
marker_colors.data(),
GPU_USAGE_STATIC,
"Marker Colors");
const int colors_ssbo_location = GPU_shader_get_ssbo_binding(shader, "marker_colors");
GPU_storagebuf_bind(colors_ssbo, colors_ssbo_location);
this->result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, this->result.domain().data_size);
this->result.unbind_as_image();
GPU_storagebuf_unbind(positions_ssbo);
GPU_storagebuf_unbind(colors_ssbo);
GPU_shader_unbind();
GPU_storagebuf_free(positions_ssbo);
GPU_storagebuf_free(colors_ssbo);
}
void KeyingScreen::compute_cpu(const float smoothness,
const Vector<float2> &marker_positions,
const Vector<float4> &marker_colors)
{
float squared_shape_parameter = math::square(1.0f / smoothness);
const int2 size = this->result.domain().data_size;
parallel_for(size, [&](const int2 texel) {
float2 normalized_pixel_location = (float2(texel) + float2(0.5f)) / float2(size);
/* Interpolate the markers using a Gaussian Radial Basis Function Interpolation with the
* reciprocal of the smoothness as the shaping parameter. Equal weights are assigned to all
* markers, so no RBF fitting is required. */
float sum_of_weights = 0.0f;
float4 weighted_sum = float4(0.0f);
for (const int64_t i : marker_positions.index_range()) {
float2 marker_position = marker_positions[i];
float2 difference = normalized_pixel_location - marker_position;
float squared_distance = math::dot(difference, difference);
float gaussian = math::exp(-squared_distance * squared_shape_parameter);
float4 marker_color = marker_colors[i];
weighted_sum += marker_color * gaussian;
sum_of_weights += gaussian;
}
weighted_sum /= sum_of_weights;
this->result.store_pixel(texel, Color(weighted_sum));
});
}
KeyingScreen::~KeyingScreen()
{
this->result.release();
}
/* --------------------------------------------------------------------
* Keying Screen Container.
*/
void KeyingScreenContainer::reset()
{
/* First, delete all cached keying screens that are no longer needed. */
for (auto &cached_keying_screens_for_id : map_.values()) {
cached_keying_screens_for_id.remove_if([](auto item) { return !item.value->needed; });
}
map_.remove_if([](auto item) { return item.value.is_empty(); });
/* Second, reset the needed status of the remaining cached keying screens to false to ready them
* to track their needed status for the next evaluation. */
for (auto &cached_keying_screens_for_id : map_.values()) {
for (auto &value : cached_keying_screens_for_id.values()) {
value->needed = false;
}
}
}
Result &KeyingScreenContainer::get(Context &context,
MovieClip *movie_clip,
MovieTrackingObject *movie_tracking_object,
float smoothness)
{
const KeyingScreenKey key(context.get_frame_number(), smoothness);
/* We concatenate the movie clip ID name with the tracking object name to cache multiple tracking
* objects per movie clip. */
const std::string library_key = movie_clip->id.lib ? movie_clip->id.lib->id.name : "";
const std::string id_key = std::string(movie_clip->id.name) + library_key;
const std::string object_key = id_key + movie_tracking_object->name;
auto &cached_keying_screens_for_id = map_.lookup_or_add_default(object_key);
/* Invalidate the cache for that movie clip if it was changed since it was cached. */
if (!cached_keying_screens_for_id.is_empty() &&
movie_clip->runtime.last_update != update_counts_.lookup(id_key))
{
cached_keying_screens_for_id.clear();
}
auto &keying_screen = *cached_keying_screens_for_id.lookup_or_add_cb(key, [&]() {
return std::make_unique<KeyingScreen>(context, movie_clip, movie_tracking_object, smoothness);
});
/* Store the current update count to later compare to and check if the movie clip changed. */
update_counts_.add_overwrite(id_key, movie_clip->runtime.last_update);
keying_screen.needed = true;
return keying_screen.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,189 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cmath>
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_index_range.hh"
#include "BLI_math_filter.hh"
#include "COM_context.hh"
#include "COM_morphological_distance_feather_weights.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Morphological Distance Feather Weights Key.
*/
MorphologicalDistanceFeatherWeightsKey::MorphologicalDistanceFeatherWeightsKey(int type,
float radius)
: type(type), radius(radius)
{
}
uint64_t MorphologicalDistanceFeatherWeightsKey::hash() const
{
return get_default_hash(type, radius);
}
bool operator==(const MorphologicalDistanceFeatherWeightsKey &a,
const MorphologicalDistanceFeatherWeightsKey &b)
{
return a.type == b.type && a.radius == b.radius;
}
/* --------------------------------------------------------------------
* Morphological Distance Feather Weights.
*/
MorphologicalDistanceFeatherWeights::MorphologicalDistanceFeatherWeights(Context &context,
int type,
int radius)
: weights(context.create_result(ResultType::Float)),
falloffs(context.create_result(ResultType::Float))
{
Result weights_cpu = this->compute_weights(context, radius);
Result falloffs_cpu = this->compute_distance_falloffs(context, type, radius);
if (context.use_gpu()) {
Result weights_gpu = weights_cpu.upload_to_gpu(false);
this->weights.share_data(weights_gpu);
weights_gpu.release();
Result falloffs_gpu = falloffs_cpu.upload_to_gpu(false);
this->falloffs.share_data(falloffs_gpu);
falloffs_gpu.release();
}
else {
this->weights.share_data(weights_cpu);
this->falloffs.share_data(falloffs_cpu);
}
weights_cpu.release();
falloffs_cpu.release();
}
MorphologicalDistanceFeatherWeights::~MorphologicalDistanceFeatherWeights()
{
weights.release();
falloffs.release();
}
Result MorphologicalDistanceFeatherWeights::compute_weights(Context &context, int radius)
{
Result weights_cpu = context.create_result(ResultType::Float);
/* The size of filter is double the radius plus 1, but since the filter is symmetric, we only
* compute half of it and no doubling happens. We add 1 to make sure the filter size is always
* odd and there is a center weight. */
const int size = radius + 1;
weights_cpu.allocate_texture(Domain(int2(size, 1)), false, ResultStorageType::CPU);
float sum = 0.0f;
/* First, compute the center weight. */
const float center_weight = math::filter_kernel_value(math::FilterKernel::Gauss, 0.0f);
weights_cpu.store_pixel(int2(0, 0), center_weight);
sum += center_weight;
/* Second, compute the other weights in the positive direction, making sure to add double the
* weight to the sum of weights because the filter is symmetric and we only loop over half of
* it. Skip the center weight already computed by dropping the front index. */
const float scale = radius > 0.0f ? 1.0f / radius : 0.0f;
for (const int i : IndexRange(size).drop_front(1)) {
const float weight = math::filter_kernel_value(math::FilterKernel::Gauss, i * scale);
weights_cpu.store_pixel(int2(i, 0), weight);
sum += weight * 2.0f;
}
/* Finally, normalize the weights. */
for (const int i : IndexRange(size)) {
const int2 texel = int2(i, 0);
weights_cpu.store_pixel(texel, weights_cpu.load_pixel<float>(texel) / sum);
}
return weights_cpu;
}
/* Computes a falloff that is equal to 1 at an input of zero and decrease to zero at an input of 1,
* with the rate of decrease depending on the falloff type. */
static float compute_distance_falloff(int type, float x)
{
x = 1.0f - x;
switch (type) {
case PROP_SMOOTH:
return 3.0f * x * x - 2.0f * x * x * x;
case PROP_SPHERE:
return std::sqrt(2.0f * x - x * x);
case PROP_ROOT:
return std::sqrt(x);
case PROP_SHARP:
return x * x;
case PROP_INVSQUARE:
return x * (2.0f - x);
case PROP_LIN:
return x;
default:
BLI_assert_unreachable();
return x;
}
}
Result MorphologicalDistanceFeatherWeights::compute_distance_falloffs(Context &context,
int type,
int radius)
{
Result falloffs_cpu = context.create_result(ResultType::Float);
/* The size of the distance falloffs is double the radius plus 1, but since the falloffs are
* symmetric, we only compute half of them and no doubling happens. We add 1 to make sure the
* falloffs size is always odd and there is a center falloff. */
const int size = radius + 1;
falloffs_cpu.allocate_texture(Domain(int2(size, 1)), false, ResultStorageType::CPU);
/* Compute the distance falloffs in the positive direction only, because the falloffs are
* symmetric. */
const float scale = radius > 0.0f ? 1.0f / radius : 0.0f;
for (const int i : IndexRange(size)) {
falloffs_cpu.store_pixel(int2(i, 0), compute_distance_falloff(type, i * scale));
}
return falloffs_cpu;
}
/* --------------------------------------------------------------------
* Morphological Distance Feather Weights Container.
*/
void MorphologicalDistanceFeatherWeightsContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
MorphologicalDistanceFeatherWeights &MorphologicalDistanceFeatherWeightsContainer::get(
Context &context, int type, int radius)
{
const MorphologicalDistanceFeatherWeightsKey key(type, radius);
auto &weights = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<MorphologicalDistanceFeatherWeights>(context, type, radius);
});
weights.needed = true;
return weights;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,690 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include <string>
#include "BLI_assert.h"
#include "BLI_hash.hh"
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "GPU_capabilities.hh"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "GPU_uniform_buffer.hh"
#include "gpu_shader_create_info.hh"
#include "COM_context.hh"
#include "COM_ocio_color_space_conversion_shader.hh"
#include "COM_result.hh"
#include "libocio_display_processor.hh"
#include "CLG_log.h"
#include <OpenColorIO/OpenColorIO.h>
namespace blender::compositor {
static CLG_LogRef LOG = {"compositor.gpu"};
/* --------------------------------------------------------------------
* GPU Shader Creator.
*/
namespace OCIO = OCIO_NAMESPACE;
using namespace blender::gpu::shader;
/* A subclass of OCIO::GpuShaderCreator that constructs the shader using a ShaderCreateInfo. The
* Create method should be used to construct the creator, then the extractGpuShaderInfo() method of
* the appropriate OCIO::GPUProcessor should be called passing in the creator. After construction,
* the constructed compute shader can be used by calling the bind_shader_and_resources() method,
* followed by binding the input texture and output image using their names input_sampler_name()
* and output_image_name(), following by dispatching the shader on the domain of the input, and
* finally calling the unbind_shader_and_resources() method.
*
* Upon calling the extractGpuShaderInfo(), all the transforms in the GPU processor will add their
* needed resources by calling the respective addUniform() and add[3D]Texture() methods. Then, the
* shader code of all transforms will be generated and passed to the createShaderText() method,
* generating the full code of the processor. Finally, the finalize() method will be called to
* finally create the shader. */
class GPUShaderCreator : public OCIO::GpuShaderCreator {
public:
static std::shared_ptr<GPUShaderCreator> Create(ResultPrecision precision)
{
std::shared_ptr<GPUShaderCreator> instance = std::make_shared<GPUShaderCreator>();
instance->setLanguage(OCIO::GPU_LANGUAGE_GLSL_4_0);
instance->precision_ = precision;
return instance;
}
/* Not used, but needs to be overridden, so return a nullptr. */
OCIO::GpuShaderCreatorRcPtr clone() const override
{
return OCIO::GpuShaderCreatorRcPtr();
}
/* This is ignored since we query using our own GPU capabilities system. */
void setTextureMaxWidth(uint /*max_width*/) override {}
uint getTextureMaxWidth() const noexcept override
{
return GPU_max_texture_size();
}
#if OCIO_VERSION_HEX >= 0x02030000
void setAllowTexture1D(bool allowed) override
{
allow_texture_1D_ = allowed;
}
bool getAllowTexture1D() const override
{
return allow_texture_1D_;
}
#endif
bool addUniform(const char *name, const DoubleGetter &get_double) override
{
/* Check if a resource exists with the same name and assert if it is the case, returning false
* indicates failure to add the uniform for the shader creator. */
if (!resource_names_.add(std::make_unique<std::string>(name))) {
BLI_assert_unreachable();
return false;
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.push_constant(Type::float_t, resource_name);
float_uniforms_.add(resource_name, get_double);
return true;
}
bool addUniform(const char *name, const BoolGetter &get_bool) override
{
/* Check if a resource exists with the same name and assert if it is the case, returning false
* indicates failure to add the uniform for the shader creator. */
if (!resource_names_.add(std::make_unique<std::string>(name))) {
BLI_assert_unreachable();
return false;
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
const std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.push_constant(Type::bool_t, resource_name);
boolean_uniforms_.add(name, get_bool);
return true;
}
bool addUniform(const char *name, const Float3Getter &get_float3) override
{
/* Check if a resource exists with the same name and assert if it is the case, returning false
* indicates failure to add the uniform for the shader creator. */
if (!resource_names_.add(std::make_unique<std::string>(name))) {
BLI_assert_unreachable();
return false;
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.push_constant(Type::float3_t, resource_name);
vector_uniforms_.add(resource_name, get_float3);
return true;
}
bool addUniform(const char *name,
const SizeGetter &get_size,
const VectorFloatGetter &get_vector_float
#if OCIO_VERSION_HEX >= 0x02050000
,
const uint /*maxSize*/
#endif
) override
{
/* Check if a resource exists with the same name and assert if it is the case, returning false
* indicates failure to add the uniform for the shader creator. */
if (!resource_names_.add(std::make_unique<std::string>(name))) {
BLI_assert_unreachable();
return false;
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.uniform_buf(buffers_sizes_.size(), "float", resource_name);
float_buffers_.add(resource_name, get_vector_float);
buffers_sizes_.add(resource_name, get_size);
return true;
}
bool addUniform(const char *name,
const SizeGetter &get_size,
const VectorIntGetter &get_vector_int
#if OCIO_VERSION_HEX >= 0x02050000
,
const uint /*maxSize*/
#endif
) override
{
/* Check if a resource exists with the same name and assert if it is the case, returning false
* indicates failure to add the uniform for the shader creator. */
if (!resource_names_.add(std::make_unique<std::string>(name))) {
BLI_assert_unreachable();
return false;
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.uniform_buf(buffers_sizes_.size(), "int", resource_name);
int_buffers_.add(name, get_vector_int);
buffers_sizes_.add(name, get_size);
return true;
}
#if OCIO_VERSION_HEX >= 0x02050000
uint
#else
void
#endif
addTexture(const char *texture_name,
const char *sampler_name,
uint width,
uint height,
TextureType channel,
#if OCIO_VERSION_HEX >= 0x02030000
OCIO::GpuShaderDesc::TextureDimensions dimensions,
#endif
OCIO::Interpolation interpolation,
const float *values) override
{
/* Check if a resource exists with the same name and assert if it is the case. */
if (!resource_names_.add(std::make_unique<std::string>(sampler_name))) {
BLI_assert_unreachable();
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
const std::string &resource_name = *resource_names_[resource_names_.size() - 1];
gpu::Texture *texture;
const gpu::TextureFormat base_format = (channel == TEXTURE_RGB_CHANNEL) ?
gpu::TextureFormat::SFLOAT_32_32_32 :
gpu::TextureFormat::SFLOAT_32;
const gpu::TextureFormat texture_format = Result::gpu_texture_format(base_format, precision_);
/* A height of 1 indicates a 1D texture according to the OCIO API. */
#if OCIO_VERSION_HEX >= 0x02030000
if (dimensions == OCIO::GpuShaderDesc::TEXTURE_1D)
#else
if (height == 1)
#endif
{
texture = GPU_texture_create_1d(
texture_name, width, 1, texture_format, GPU_TEXTURE_USAGE_SHADER_READ, values);
shader_create_info_.sampler(textures_.size() + 1, ImageType::Float1D, resource_name);
}
else {
texture = GPU_texture_create_2d(
texture_name, width, height, 1, texture_format, GPU_TEXTURE_USAGE_SHADER_READ, values);
shader_create_info_.sampler(textures_.size() + 1, ImageType::Float2D, resource_name);
}
GPU_texture_filter_mode(texture, interpolation != OCIO::INTERP_NEAREST);
textures_.add(sampler_name, texture);
#if OCIO_VERSION_HEX >= 0x02050000
return textures_.size() - 1;
#endif
}
#if OCIO_VERSION_HEX >= 0x02050000
uint
#else
void
#endif
add3DTexture(const char *texture_name,
const char *sampler_name,
uint size,
OCIO::Interpolation interpolation,
const float *values) override
{
/* Check if a resource exists with the same name and assert if it is the case. */
if (!resource_names_.add(std::make_unique<std::string>(sampler_name))) {
BLI_assert_unreachable();
}
/* Don't use the name argument directly since ShaderCreateInfo only stores references to
* resource names, instead, use the name that is stored in resource_names_. */
const std::string &resource_name = *resource_names_[resource_names_.size() - 1];
shader_create_info_.sampler(textures_.size() + 1, ImageType::Float3D, resource_name);
gpu::Texture *texture = GPU_texture_create_3d(
texture_name,
size,
size,
size,
1,
Result::gpu_texture_format(gpu::TextureFormat::SFLOAT_32_32_32, precision_),
GPU_TEXTURE_USAGE_SHADER_READ,
values);
GPU_texture_filter_mode(texture, interpolation != OCIO::INTERP_NEAREST);
textures_.add(sampler_name, texture);
#if OCIO_VERSION_HEX >= 0x02050000
return textures_.size() - 1;
#endif
}
/* This gets called before the finalize() method to construct the shader code. We just
* concatenate the code except for the declarations section. That's because the ShaderCreateInfo
* will add the declaration itself. */
void createShaderText(const char * /*parameter_declarations*/,
#if OCIO_VERSION_HEX >= 0x02050000
const char * /*texture_declarations*/,
#endif
const char *helper_methods,
const char *function_header,
const char *function_body,
const char *function_footer) override
{
shader_code_ += helper_methods;
shader_code_ += function_header;
shader_code_ += function_body;
shader_code_ += function_footer;
}
/* This gets called when all resources were added using the respective addUniform() or
* add[3D]Texture() methods and the shader code was generated using the createShaderText()
* method. That is, we are ready to complete the ShaderCreateInfo and create a shader from it. */
void finalize() override
{
GpuShaderCreator::finalize();
shader_create_info_.local_group_size(16, 16);
shader_create_info_.sampler(0, ImageType::Float2D, input_sampler_name());
shader_create_info_.builtins(BuiltinBits::GLOBAL_INVOCATION_ID);
shader_create_info_.push_constant(Type::bool_t, "premultiply_output");
shader_create_info_.image(0,
Result::gpu_texture_format(ResultType::Color, precision_),
Qualifier::write,
ImageReadWriteType::Float2D,
output_image_name());
shader_create_info_.compute_source("gpu_shader_compositor_ocio_processor.glsl");
shader_create_info_.generated_sources.append(
{"gpu_shader_compositor_ocio_processor_lib.glsl",
{},
GPU_shader_preprocess_source(shader_code_, shader_create_info_)});
GPUShaderCreateInfo *info = reinterpret_cast<GPUShaderCreateInfo *>(&shader_create_info_);
shader_ = GPU_shader_create_from_info(info);
}
gpu::Shader *bind_shader_and_resources()
{
if (!shader_) {
return nullptr;
}
GPU_shader_bind(shader_);
for (auto item : float_uniforms_.items()) {
GPU_shader_uniform_1f(shader_, item.key.c_str(), item.value());
}
for (auto item : boolean_uniforms_.items()) {
GPU_shader_uniform_1b(shader_, item.key.c_str(), item.value());
}
for (auto item : vector_uniforms_.items()) {
GPU_shader_uniform_3fv(shader_, item.key.c_str(), item.value().data());
}
for (auto item : float_buffers_.items()) {
gpu::UniformBuf *buffer = GPU_uniformbuf_create_ex(
buffers_sizes_.lookup(item.key)(), item.value(), item.key.c_str());
const int ubo_location = GPU_shader_get_ubo_binding(shader_, item.key.c_str());
GPU_uniformbuf_bind(buffer, ubo_location);
uniform_buffers_.append(buffer);
}
for (auto item : int_buffers_.items()) {
gpu::UniformBuf *buffer = GPU_uniformbuf_create_ex(
buffers_sizes_.lookup(item.key)(), item.value(), item.key.c_str());
const int ubo_location = GPU_shader_get_ubo_binding(shader_, item.key.c_str());
GPU_uniformbuf_bind(buffer, ubo_location);
uniform_buffers_.append(buffer);
}
for (auto item : textures_.items()) {
const int texture_image_unit = GPU_shader_get_sampler_binding(shader_, item.key.c_str());
GPU_texture_bind(item.value, texture_image_unit);
}
return shader_;
}
void unbind_shader_and_resources()
{
for (gpu::UniformBuf *buffer : uniform_buffers_) {
GPU_uniformbuf_unbind(buffer);
GPU_uniformbuf_free(buffer);
}
for (gpu::Texture *texture : textures_.values()) {
GPU_texture_unbind(texture);
}
GPU_shader_unbind();
}
const char *input_sampler_name()
{
return "input_tx";
}
const char *output_image_name()
{
return "output_img";
}
~GPUShaderCreator() override
{
for (gpu::Texture *texture : textures_.values()) {
GPU_texture_free(texture);
}
GPU_shader_free(shader_);
}
private:
/* The processor shader and the ShaderCreateInfo used to construct it. Constructed and
* initialized in the finalize() method. */
gpu::Shader *shader_ = nullptr;
ShaderCreateInfo shader_create_info_ = ShaderCreateInfo("OCIO_Processor");
/* Stores the generated OCIOMain function as well as a number of helper functions. Initialized in
* the createShaderText() method. */
std::string shader_code_;
/* Maps that associates the name of a uniform with a getter function that returns its value.
* Initialized in the respective addUniform() methods. */
Map<std::string, DoubleGetter> float_uniforms_;
Map<std::string, BoolGetter> boolean_uniforms_;
Map<std::string, Float3Getter> vector_uniforms_;
/* Maps that associates the name of uniform buffer objects with a getter function that returns
* its values. Initialized in the respective addUniform() methods. */
Map<std::string, VectorFloatGetter> float_buffers_;
Map<std::string, VectorIntGetter> int_buffers_;
/* A map that associates the name of uniform buffer objects with a getter functions that returns
* its number of elements. Initialized in the respective addUniform() methods. */
Map<std::string, SizeGetter> buffers_sizes_;
/* A map that associates the name of a sampler with its corresponding texture. Initialized in the
* addTexture() and add3DTexture() methods. */
Map<std::string, gpu::Texture *> textures_;
/* A vector set that stores the names of all the resources used by the shader. This is used to:
* 1. Check for name collisions when adding new resources.
* 2. Store the resource names throughout the construction of the shader since the
* ShaderCreateInfo class only stores references to resources names. */
VectorSet<std::unique_ptr<std::string>> resource_names_;
/* A vectors that stores the created uniform buffers when bind_shader_and_resources() is called,
* so that they can be properly unbound and freed in the unbind_shader_and_resources() method. */
Vector<gpu::UniformBuf *> uniform_buffers_;
#if OCIO_VERSION_HEX >= 0x02030000
/* Allow creating 1D textures, or only use 2D textures. */
bool allow_texture_1D_ = true;
#endif
/* The precision of the OCIO resources as well as the output image. */
ResultPrecision precision_;
};
/* ------------------------------------------------------------------------------------------------
* OCIO Color Space Conversion Shader Key.
*/
OCIOColorSpaceConversionShaderKey::OCIOColorSpaceConversionShaderKey(
const std::string &source, const std::string &target, const std::string &config_cache_id)
: source(source), target(target), config_cache_id(config_cache_id)
{
}
uint64_t OCIOColorSpaceConversionShaderKey::hash() const
{
return get_default_hash(source, target, config_cache_id);
}
bool operator==(const OCIOColorSpaceConversionShaderKey &a,
const OCIOColorSpaceConversionShaderKey &b)
{
return a.source == b.source && a.target == b.target && a.config_cache_id == b.config_cache_id;
}
/* --------------------------------------------------------------------
* OCIO Color Space Conversion Shader.
*/
OCIOColorSpaceConversionShader::OCIOColorSpaceConversionShader(Context &context,
std::string source,
std::string target)
{
/* Create a GPU shader creator and construct it based on the transforms in the default GPU
* processor. */
shader_creator_ = GPUShaderCreator::Create(context.get_precision());
/* Get a GPU processor that transforms the source color space to the target color space. */
try {
OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();
OCIO::ConstProcessorRcPtr processor = config->getProcessor(source.c_str(), target.c_str());
OCIO::ConstGPUProcessorRcPtr gpu_processor = processor->getDefaultGPUProcessor();
auto ocio_shader_creator = std::static_pointer_cast<OCIO::GpuShaderCreator>(shader_creator_);
gpu_processor->extractGpuShaderInfo(ocio_shader_creator);
}
catch (const OCIO::Exception &e) {
CLOG_ERROR(&LOG, "Failed to create OpenColorIO shader: %s", e.what());
}
}
gpu::Shader *OCIOColorSpaceConversionShader::bind_shader_and_resources()
{
return shader_creator_->bind_shader_and_resources();
}
void OCIOColorSpaceConversionShader::unbind_shader_and_resources()
{
shader_creator_->unbind_shader_and_resources();
}
const char *OCIOColorSpaceConversionShader::input_sampler_name()
{
return shader_creator_->input_sampler_name();
}
const char *OCIOColorSpaceConversionShader::output_image_name()
{
return shader_creator_->output_image_name();
}
/* --------------------------------------------------------------------
* OCIO Color Space Conversion Shader Container.
*/
void OCIOColorSpaceConversionShaderContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
OCIOColorSpaceConversionShader &OCIOColorSpaceConversionShaderContainer::get(Context &context,
std::string source,
std::string target)
{
/* Use the config cache ID in the cache key in case the configuration changed at runtime. */
std::string config_cache_id = OCIO::GetCurrentConfig()->getCacheID();
const OCIOColorSpaceConversionShaderKey key(source, target, config_cache_id);
OCIOColorSpaceConversionShader &shader = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<OCIOColorSpaceConversionShader>(context, source, target);
});
shader.needed = true;
return shader;
}
/* ------------------------------------------------------------------------------------------------
* OCIO To Display Shader Key.
*/
OCIOToDisplayShaderKey::OCIOToDisplayShaderKey(const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse,
const std::string &config_cache_id)
: display_device(display_settings.display_device),
view_transform(view_settings.view_transform),
look(view_settings.look),
inverse(inverse),
config_cache_id(config_cache_id)
{
}
uint64_t OCIOToDisplayShaderKey::hash() const
{
return get_default_hash(
get_default_hash(display_device, view_transform, look, (inverse) ? "inverse" : "forward"),
config_cache_id);
}
bool operator==(const OCIOToDisplayShaderKey &a, const OCIOToDisplayShaderKey &b)
{
return a.display_device == b.display_device && a.view_transform == b.view_transform &&
a.look == b.look && a.inverse == b.inverse && a.config_cache_id == b.config_cache_id;
}
/* --------------------------------------------------------------------
* OCIO To Display Shader.
*/
OCIOToDisplayShader::OCIOToDisplayShader(Context &context,
const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse)
{
/* Create a GPU shader creator and construct it based on the transforms in the default GPU
* processor. */
shader_creator_ = GPUShaderCreator::Create(context.get_precision());
/* Get a GPU processor that transforms the display_device color space to the view_transform color
* space. */
try {
OCIO::ConstConfigRcPtr config = OCIO::GetCurrentConfig();
OCIO::TransformRcPtr group = ocio::create_ocio_display_transform(
config,
display_settings.display_device,
view_settings.view_transform,
view_settings.look,
"scene_linear");
if (inverse) {
group->setDirection(OCIO::TRANSFORM_DIR_INVERSE);
}
OCIO::ConstProcessorRcPtr processor = config->getProcessor(group);
OCIO::ConstGPUProcessorRcPtr gpu_processor = processor->getDefaultGPUProcessor();
auto ocio_shader_creator = std::static_pointer_cast<OCIO::GpuShaderCreator>(shader_creator_);
gpu_processor->extractGpuShaderInfo(ocio_shader_creator);
}
catch (const OCIO::Exception &e) {
CLOG_ERROR(&LOG, "Failed to create OpenColorIO shader: %s", e.what());
}
}
gpu::Shader *OCIOToDisplayShader::bind_shader_and_resources()
{
return shader_creator_->bind_shader_and_resources();
}
void OCIOToDisplayShader::unbind_shader_and_resources()
{
shader_creator_->unbind_shader_and_resources();
}
const char *OCIOToDisplayShader::input_sampler_name()
{
return shader_creator_->input_sampler_name();
}
const char *OCIOToDisplayShader::output_image_name()
{
return shader_creator_->output_image_name();
}
/* --------------------------------------------------------------------
* OCIO To Display Shader Container.
*/
void OCIOToDisplayShaderContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to
* track their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
OCIOToDisplayShader &OCIOToDisplayShaderContainer::get(
Context &context,
const ColorManagedDisplaySettings &display_settings,
const ColorManagedViewSettings &view_settings,
const bool inverse)
{
/* Use the config cache ID in the cache key in case the configuration changed at runtime. */
std::string config_cache_id = OCIO::GetCurrentConfig()->getCacheID();
const OCIOToDisplayShaderKey key(display_settings, view_settings, inverse, config_cache_id);
OCIOToDisplayShader &shader = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<OCIOToDisplayShader>(
context, display_settings, view_settings, inverse);
});
shader.needed = true;
return shader;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,138 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include "BLI_smaa_textures.h"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_smaa_precomputed_textures.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* SMAA Precomputed Textures.
*/
SMAAPrecomputedTextures::SMAAPrecomputedTextures(Context &context)
: search_texture(context.create_result(ResultType::Float)),
area_texture(context.create_result(ResultType::Float2))
{
if (context.use_gpu()) {
this->compute_gpu();
}
else {
this->compute_cpu();
}
}
SMAAPrecomputedTextures::~SMAAPrecomputedTextures()
{
GPU_TEXTURE_FREE_SAFE(search_texture_);
GPU_TEXTURE_FREE_SAFE(area_texture_);
this->search_texture.release();
this->area_texture.release();
}
void SMAAPrecomputedTextures::bind_search_texture(gpu::Shader *shader,
const char *sampler_name) const
{
const int texture_image_unit = GPU_shader_get_sampler_binding(shader, sampler_name);
GPU_texture_bind(search_texture_, texture_image_unit);
}
void SMAAPrecomputedTextures::unbind_search_texture() const
{
GPU_texture_unbind(search_texture_);
}
void SMAAPrecomputedTextures::bind_area_texture(gpu::Shader *shader,
const char *sampler_name) const
{
const int texture_image_unit = GPU_shader_get_sampler_binding(shader, sampler_name);
GPU_texture_bind(area_texture_, texture_image_unit);
}
void SMAAPrecomputedTextures::unbind_area_texture() const
{
GPU_texture_unbind(area_texture_);
}
void SMAAPrecomputedTextures::compute_gpu()
{
search_texture_ = GPU_texture_create_2d("SMAA Search",
SEARCHTEX_WIDTH,
SEARCHTEX_HEIGHT,
1,
gpu::TextureFormat::UNORM_8,
GPU_TEXTURE_USAGE_SHADER_READ,
nullptr);
GPU_texture_update(search_texture_, GPU_DATA_UBYTE, searchTexBytes);
GPU_texture_filter_mode(search_texture_, true);
area_texture_ = GPU_texture_create_2d("SMAA Area",
AREATEX_WIDTH,
AREATEX_HEIGHT,
1,
gpu::TextureFormat::UNORM_8_8,
GPU_TEXTURE_USAGE_SHADER_READ,
nullptr);
GPU_texture_update(area_texture_, GPU_DATA_UBYTE, areaTexBytes);
GPU_texture_filter_mode(area_texture_, true);
}
void SMAAPrecomputedTextures::compute_cpu()
{
const int2 search_texture_size = int2(SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT);
search_texture.allocate_texture(Domain(search_texture_size), false);
parallel_for(search_texture_size, [&](const int2 texel) {
const float value = searchTexBytes[int64_t(texel.y) * search_texture_size.x + texel.x] /
255.0f;
search_texture.store_pixel(texel, value);
});
const int2 area_texture_size = int2(AREATEX_WIDTH, AREATEX_HEIGHT);
area_texture.allocate_texture(Domain(area_texture_size), false);
parallel_for(area_texture_size, [&](const int2 texel) {
const float2 value = float2(uchar2(areaTexBytes +
(int64_t(texel.y) * area_texture_size.x + texel.x) * 2)) /
255.0f;
area_texture.store_pixel(texel, value);
});
}
/* ------------------------------------------------------------------------------------------------
* SMAA Precomputed Textures Container.
*/
void SMAAPrecomputedTexturesContainer::reset()
{
/* First, delete the textures if they are no longer needed. */
if (textures_ && !textures_->needed) {
textures_.reset();
}
/* Second, if they were not deleted, reset their needed status to false to ready them to track
* their needed status for the next evaluation. */
if (textures_) {
textures_->needed = false;
}
}
SMAAPrecomputedTextures &SMAAPrecomputedTexturesContainer::get(Context &context)
{
if (!textures_) {
textures_ = std::make_unique<SMAAPrecomputedTextures>(context);
}
textures_->needed = true;
return *textures_;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,295 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <limits>
#include <string>
#include "BLI_assert.h"
#include "BLI_hash.hh"
#include "BLI_memory_utils.hh"
#include "BLI_path_utils.hh"
#include "BLI_rect.h"
#include "BLI_string.h"
#include "DNA_packedFile_types.h"
#include "DNA_vfont_types.h"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_vfont.hh"
#include "BLF_api.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_string_image.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* String Image Key.
*/
StringImageKey::StringImageKey(const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width)
: string(string),
font(font),
size(size),
horizontal_alignment(horizontal_alignment),
vertical_alignment(vertical_alignment),
wrap_width(wrap_width)
{
}
uint64_t StringImageKey::hash() const
{
return get_default_hash(
get_default_hash(string, font, size, horizontal_alignment, vertical_alignment),
wrap_width.value_or(-1));
}
/* --------------------------------------------------------------------
* String Image.
*/
/* Loads a BLF font from the given font ID and return its identifier. Unloading the font is the
* responsibility of the caller. */
static int load_font(const VFont *font)
{
if (!font || BKE_vfont_is_builtin(font)) {
return BLF_load_default(true);
}
if (font->packedfile != nullptr) {
char name[MAX_ID_FULL_NAME];
BKE_id_full_name_get(name, &font->id, 0);
return BLF_load_mem_unique(
name, static_cast<const uchar *>(font->packedfile->data), font->packedfile->size);
}
char file_path[FILE_MAX];
STRNCPY(file_path, font->filepath);
BLI_path_abs(file_path, ID_BLEND_PATH_FROM_GLOBAL(&font->id));
return BLF_load_unique(file_path);
}
/* Computes the horizontal position in pixels from which the line with the given width should be
* drawn. The BLF module draws from the left most edge of the line. The start offset is an offset
* that is intrinsic to the used font and thus should be canceled here to draw right from the edge
* of the image. The total width is the total width of the image and is the maximum width of all
* lines. */
static float compute_draw_horizontal_position(const int start_offset,
const int line_width,
const int total_width,
const HorizontalAlignment alignment)
{
switch (alignment) {
case HorizontalAlignment::Left:
return float(-start_offset);
case HorizontalAlignment::Center:
return -start_offset + (total_width - line_width) / 2.0f;
case HorizontalAlignment::Right:
return float(-start_offset + total_width - line_width);
}
BLI_assert_unreachable();
return float(-start_offset);
}
/* Computes the vertical position in pixels from which the line with the given index should be
* drawn. Assuming there are the given number of lines with each having the given height and
* descender. The BLF module draws from the baseline of the line starting from the lower left
* origin of the image, therefore, we need to subtract the negative descender such that the line is
* fully drawn in the image. */
static float compute_draw_vertical_position(const int lines_count,
const int line_index,
const int line_height,
const int descender)
{
return (lines_count - 1 - line_index) * line_height - float(descender);
}
/* Computes the horizontal offset in pixels depending on the alignment. We also restore the start
* offset that was canceled during drawing, see compute_draw_horizontal_position. */
static float compute_horizontal_offset(const int start_offset,
const int total_width,
const HorizontalAlignment alignment)
{
switch (alignment) {
case HorizontalAlignment::Left:
return start_offset + total_width / 2.0f;
case HorizontalAlignment::Center:
return float(start_offset);
case HorizontalAlignment::Right:
return start_offset - total_width / 2.0f;
}
BLI_assert_unreachable();
return float(start_offset);
}
/* Computes the vertical offset in pixels depending on the alignment. Assuming the image has the
* given total height, the lines have the given height and negative descender. */
static float compute_vertical_offset(const int total_height,
const int line_height,
const int descender,
const VerticalAlignment alignment)
{
switch (alignment) {
case VerticalAlignment::Top:
return -total_height / 2.0f;
case VerticalAlignment::TopBaseline:
return -total_height / 2.0f + line_height + descender;
case VerticalAlignment::Middle:
return 0.0f;
case VerticalAlignment::BottomBaseline:
return total_height / 2.0f + descender;
case VerticalAlignment::Bottom:
return total_height / 2.0f;
}
BLI_assert_unreachable();
return float(descender);
}
StringImage::StringImage(Context &context,
const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width)
: result(context.create_result(ResultType::Float))
{
if (string.empty() || !font || size <= 0.0f) {
return;
}
const int font_identifier = load_font(font);
if (font_identifier == -1) {
return;
}
BLI_SCOPED_DEFER([&]() { BLF_unload_id(font_identifier); });
BLF_size(font_identifier, size);
/* Only fallback to default fonts for unknown characters if this is the built-in font, otherwise,
* the image might change across setups and versions if fonts we fallback to change. */
if (!BLF_is_builtin(font_identifier)) {
BLF_enable(font_identifier, BLF_NO_FALLBACK);
}
Vector<StringRef> lines = BLF_string_wrap(
font_identifier, string, wrap_width.value_or(-1), BLFWrapMode::Typographical);
/* Compute the width of all lines as well as their starting offset. The starting offset will be
* zero in most fonts, but some special fonts might start before or after the zero point. */
int total_width = 0;
Array<int> line_widths(lines.size());
int start_offset = std::numeric_limits<int>::max();
for (const int64_t i : lines.index_range()) {
rcti line_bounding_box;
BLF_boundbox(font_identifier, lines[i].data(), lines[i].size(), &line_bounding_box);
line_widths[i] = BLI_rcti_size_x(&line_bounding_box);
total_width = math::max(total_width, line_widths[i]);
start_offset = math::min(start_offset, line_bounding_box.xmin);
}
const int line_height = BLF_height_max(font_identifier);
const int total_height = line_height * lines.size();
/* Fill the background with alpha since the draws function does not initialize the background. */
Result result_cpu = context.create_result(ResultType::Float);
result_cpu.allocate_texture(int2(total_width, total_height), false, ResultStorageType::CPU);
parallel_for(result_cpu.domain().data_size,
[&](const int2 texel) { result_cpu.store_pixel(texel, 0.0f); });
BLF_buffer_col(font_identifier, Color(1.0f, 1.0f, 1.0f, 1.0f));
BLF_buffer(font_identifier,
static_cast<float *>(result_cpu.cpu_data_for_write().data()),
nullptr,
total_width,
total_height,
1,
nullptr);
/* Draw each of lines in the appropriate position. */
const int descender = BLF_descender(font_identifier);
for (const int64_t i : lines.index_range()) {
const float vertical_position = compute_draw_vertical_position(
lines.size(), i, line_height, descender);
const float horizontal_position = compute_draw_horizontal_position(
start_offset, line_widths[i], total_width, horizontal_alignment);
BLF_position(font_identifier, horizontal_position, vertical_position, 0.0f);
BLF_draw_buffer(font_identifier, lines[i].data(), lines[i].size());
}
BLF_buffer(font_identifier, nullptr, nullptr, 0, 0, 1, nullptr);
/* Move the image to account for the requested alignment. */
const float horizontal_offset = compute_horizontal_offset(
start_offset, total_width, horizontal_alignment);
const float vertical_offset = compute_vertical_offset(
total_height, line_height, descender, vertical_alignment);
result_cpu.domain().transformation.location() = float2(horizontal_offset, vertical_offset);
if (context.use_gpu()) {
Result result_gpu = result_cpu.upload_to_gpu(false);
this->result.share_data(result_gpu);
result_gpu.release();
}
else {
this->result.share_data(result_cpu);
}
result_cpu.release();
}
StringImage::~StringImage()
{
this->result.release();
}
/* --------------------------------------------------------------------
* String Image Container.
*/
void StringImageContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
Result &StringImageContainer::get(Context &context,
const std::string string,
const VFont *font,
const float size,
const HorizontalAlignment horizontal_alignment,
const VerticalAlignment vertical_alignment,
const std::optional<int> wrap_width)
{
const StringImageKey key(
string, font, size, horizontal_alignment, vertical_alignment, wrap_width);
auto &string_image = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<StringImage>(
context, string, font, size, horizontal_alignment, vertical_alignment, wrap_width);
});
string_image.needed = true;
return string_image.result;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,147 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_index_range.hh"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_symmetric_blur_weights.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Symmetric Blur Weights Key.
*/
SymmetricBlurWeightsKey::SymmetricBlurWeightsKey(math::FilterKernel type, float2 radius)
: type(type), radius(radius)
{
}
uint64_t SymmetricBlurWeightsKey::hash() const
{
return get_default_hash(type, radius.x, radius.y);
}
bool operator==(const SymmetricBlurWeightsKey &a, const SymmetricBlurWeightsKey &b)
{
return a.type == b.type && a.radius == b.radius;
}
/* --------------------------------------------------------------------
* Symmetric Blur Weights.
*/
SymmetricBlurWeights::SymmetricBlurWeights(Context &context,
math::FilterKernel type,
float2 radius)
: weights(context.create_result(ResultType::Float))
{
Result weights_cpu = context.create_result(ResultType::Float);
/* The full size of filter is double the radius plus 1, but since the filter is symmetric, we
* only compute a single quadrant of it and so no doubling happens. We add 1 to make sure the
* filter size is always odd and there is a center weight. */
const float2 scale = math::safe_divide(float2(1.0f), radius);
const int2 size = int2(math::ceil(radius)) + int2(1);
weights_cpu.allocate_texture(size, false, ResultStorageType::CPU);
float sum = 0.0f;
/* First, compute the center weight. */
const float center_weight = math::filter_kernel_value(type, 0.0f);
weights_cpu.store_pixel(int2(0, 0), center_weight);
sum += center_weight;
/* Then, compute the weights along the positive x axis, making sure to add double the weight to
* the sum of weights because the filter is symmetric and we only loop over the positive half
* of the x axis. Skip the center weight already computed by dropping the front index. */
for (const int x : IndexRange(size.x).drop_front(1)) {
const float weight = math::filter_kernel_value(type, x * scale.x);
weights_cpu.store_pixel(int2(x, 0), weight);
sum += weight * 2.0f;
}
/* Then, compute the weights along the positive y axis, making sure to add double the weight to
* the sum of weights because the filter is symmetric and we only loop over the positive half
* of the y axis. Skip the center weight already computed by dropping the front index. */
for (const int y : IndexRange(size.y).drop_front(1)) {
const float weight = math::filter_kernel_value(type, y * scale.y);
weights_cpu.store_pixel(int2(0, y), weight);
sum += weight * 2.0f;
}
/* Then, compute the other weights in the upper right quadrant, making sure to add quadruple
* the weight to the sum of weights because the filter is symmetric and we only loop over one
* quadrant of it. Skip the weights along the y and x axis already computed by dropping the
* front index. */
for (const int y : IndexRange(size.y).drop_front(1)) {
for (const int x : IndexRange(size.x).drop_front(1)) {
const float weight = math::filter_kernel_value(type, math::length(float2(x, y) * scale));
weights_cpu.store_pixel(int2(x, y), weight);
sum += weight * 4.0f;
}
}
/* Finally, normalize the weights. */
for (const int y : IndexRange(size.y)) {
for (const int x : IndexRange(size.x)) {
const int2 texel = int2(x, y);
weights_cpu.store_pixel(texel, weights_cpu.load_pixel<float>(texel) / sum);
}
}
if (context.use_gpu()) {
Result weights_gpu = weights_cpu.upload_to_gpu(false);
this->weights.share_data(weights_gpu);
weights_gpu.release();
}
else {
this->weights.share_data(weights_cpu);
}
weights_cpu.release();
}
SymmetricBlurWeights::~SymmetricBlurWeights()
{
this->weights.release();
}
/* --------------------------------------------------------------------
* Symmetric Blur Weights Container.
*/
void SymmetricBlurWeightsContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
Result &SymmetricBlurWeightsContainer::get(Context &context,
math::FilterKernel type,
float2 radius)
{
const SymmetricBlurWeightsKey key(type, radius);
auto &weights = *map_.lookup_or_add_cb(
key, [&]() { return std::make_unique<SymmetricBlurWeights>(context, type, radius); });
weights.needed = true;
return weights.weights;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,126 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <cstdint>
#include <memory>
#include "BLI_hash.hh"
#include "BLI_index_range.hh"
#include "BLI_math_base.hh"
#include "COM_context.hh"
#include "COM_result.hh"
#include "COM_symmetric_separable_blur_weights.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Symmetric Separable Blur Weights Key.
*/
SymmetricSeparableBlurWeightsKey::SymmetricSeparableBlurWeightsKey(math::FilterKernel type,
float radius)
: type(type), radius(radius)
{
}
uint64_t SymmetricSeparableBlurWeightsKey::hash() const
{
return get_default_hash(type, radius);
}
bool operator==(const SymmetricSeparableBlurWeightsKey &a,
const SymmetricSeparableBlurWeightsKey &b)
{
return a.type == b.type && a.radius == b.radius;
}
/* --------------------------------------------------------------------
* Symmetric Separable Blur Weights.
*/
SymmetricSeparableBlurWeights::SymmetricSeparableBlurWeights(Context &context,
math::FilterKernel type,
float radius)
: weights(context.create_result(ResultType::Float))
{
Result weights_cpu = context.create_result(ResultType::Float);
/* The size of filter is double the radius plus 1, but since the filter is symmetric, we only
* compute half of it and no doubling happens. We add 1 to make sure the filter size is always
* odd and there is a center weight. */
weights_cpu.allocate_texture(
Domain(int2(math::ceil(radius) + 1, 1)), false, ResultStorageType::CPU);
float sum = 0.0f;
/* First, compute the center weight. */
const float center_weight = math::filter_kernel_value(type, 0.0f);
weights_cpu.store_pixel(int2(0, 0), center_weight);
sum += center_weight;
/* Second, compute the other weights in the positive direction, making sure to add double the
* weight to the sum of weights because the filter is symmetric and we only loop over half of
* it. Skip the center weight already computed by dropping the front index. */
const float scale = radius > 0.0f ? 1.0f / radius : 0.0f;
for (const int i : IndexRange(weights_cpu.domain().data_size.x).drop_front(1)) {
const float weight = math::filter_kernel_value(type, i * scale);
weights_cpu.store_pixel(int2(i, 0), weight);
sum += weight * 2.0f;
}
/* Finally, normalize the weights. */
for (const int i : IndexRange(weights_cpu.domain().data_size.x)) {
const int2 texel = int2(i, 0);
weights_cpu.store_pixel(texel, weights_cpu.load_pixel<float>(texel) / sum);
}
if (context.use_gpu()) {
Result weights_gpu = weights_cpu.upload_to_gpu(false);
this->weights.share_data(weights_gpu);
weights_gpu.release();
}
else {
this->weights.share_data(weights_cpu);
}
weights_cpu.release();
}
SymmetricSeparableBlurWeights::~SymmetricSeparableBlurWeights()
{
this->weights.release();
}
/* --------------------------------------------------------------------
* Symmetric Separable Blur Weights Container.
*/
void SymmetricSeparableBlurWeightsContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
Result &SymmetricSeparableBlurWeightsContainer::get(Context &context,
math::FilterKernel type,
float radius)
{
const SymmetricSeparableBlurWeightsKey key(type, radius);
auto &weights = *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<SymmetricSeparableBlurWeights>(context, type, radius);
});
weights.needed = true;
return weights.weights;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,556 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* -------------------------------------------------------------------------------------------------
* Van Vliet Gaussian Coefficients.
*
* Computes the coefficients of the fourth order IIR filter approximating a Gaussian filter
* computed using Van Vliet's design method. This is based on the following paper:
*
* Van Vliet, Lucas J., Ian T. Young, and Piet W. Verbeek. "Recursive Gaussian derivative
* filters." Proceedings. Fourteenth International Conference on Pattern Recognition (Cat. No.
* 98EX170). Vol. 1. IEEE, 1998.
*
*
* The filter is computed as the cascade of a causal and a non causal sequences of second order
* difference equations as can be seen in Equation (11) in Van Vliet's paper. The coefficients are
* the same for both the causal and non causal sequences.
*
* However, to improve its numerical stability, we decompose the 4th order filter into a parallel
* bank of second order filers using the methods of partial fractions as demonstrated in the follow
* book:
*
* Oppenheim, Alan V. Discrete-time signal processing. Pearson Education India, 1999.
*
*/
#include <array>
#include <complex>
#include <cstdint>
#include <memory>
#include "BLI_assert.h"
#include "BLI_hash.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector.hh"
#include "COM_context.hh"
#include "COM_van_vliet_gaussian_coefficients.hh"
namespace blender::compositor {
/* --------------------------------------------------------------------
* Van Vliet Gaussian Coefficients Key.
*/
VanVlietGaussianCoefficientsKey::VanVlietGaussianCoefficientsKey(float sigma) : sigma(sigma) {}
uint64_t VanVlietGaussianCoefficientsKey::hash() const
{
return get_default_hash(sigma);
}
bool operator==(const VanVlietGaussianCoefficientsKey &a, const VanVlietGaussianCoefficientsKey &b)
{
return a.sigma == b.sigma;
}
/* -------------------------------------------------------------------------------------------------
* Van Vliet Gaussian Coefficients.
*/
/* Computes the variance of the Gaussian filter represented by the given poles scaled by the given
* scale factor. This is based on Equation (20) in Van Vliet's paper. */
static double compute_scaled_poles_variance(const std::array<std::complex<double>, 4> &poles,
double scale_factor)
{
std::complex<double> variance = std::complex<double>(0.0, 0.0);
for (const std::complex<double> &pole : poles) {
const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
const double phase = std::arg(pole) / scale_factor;
const std::complex<double> multiplier1 = std::polar(magnitude, phase);
const std::complex<double> multiplier2 = std::pow(magnitude - std::polar(1.0, phase), -2.0);
variance += 2.0 * multiplier1 * multiplier2;
}
/* The variance is actually real valued as guaranteed by Equations (10) and (2) since the poles
* are complex conjugate pairs. See Section 3.3 of the paper. */
return variance.real();
}
/**
* Computes the partial derivative with respect to the scale factor at the given scale factor of
* the variance of the Gaussian filter represented by the given poles scaled by the given scale
* factor. This is based on the partial derivative with respect to the scale factor of Equation
* (20) in Van Vliet's paper.
*
* The derivative is not listed in the paper, but was computed manually as the sum of the following
* for each of the poles:
*
* \code{.tex}
* \frac{
* 2a^\frac{1}{x}e^\frac{ib}{x} (e^\frac{ib}{x}+a^\frac{1}{x}) (\ln(a)-ib)
* }{
* x^2 (a^\frac{1}{x}-e^\frac{ib}{x})^3
* }
* \endcode
*
* Where "x" is the scale factor, "a" is the magnitude of the pole, and "b" is its phase.
*/
static double compute_scaled_poles_variance_derivative(
const std::array<std::complex<double>, 4> &poles, double scale_factor)
{
std::complex<double> variance_derivative = std::complex<double>(0.0, 0.0);
for (const std::complex<double> &pole : poles) {
const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
const double phase = std::arg(pole) / scale_factor;
const std::complex<double> multiplier1 = std::polar(magnitude, phase);
const std::complex<double> multiplier2 = magnitude + std::polar(1.0, phase);
const std::complex<double> multiplier3 = std::log(std::abs(pole)) -
std::complex<double>(0.0, std::arg(pole));
const std::complex<double> divisor1 = std::pow(magnitude - std::polar(1.0, phase), 3.0);
const std::complex<double> divisor2 = math::square(scale_factor);
variance_derivative += 2.0 * multiplier1 * multiplier2 * multiplier3 / (divisor1 * divisor2);
}
/* The variance derivative is actually real valued as guaranteed by Equations (10) and (2) since
* the poles are complex conjugate pairs. See Section 3.3 of the paper. */
return variance_derivative.real();
}
/* The poles were computed for a Gaussian filter with a sigma value of 2, in order to generalize
* that for any sigma value, we need to scale the poles by a certain scaling factor as described in
* Section 4.2 of Van Vliet's paper. To find the scaling factor, we start from an initial guess of
* half sigma, then iteratively improve the guess using Newton's method by computing the variance
* and its derivative based on Equation (20). */
static double find_scale_factor(const std::array<std::complex<double>, 4> &poles,
double reference_sigma)
{
const double reference_variance = math::square(reference_sigma);
/* Note that the poles were computed for a Gaussian filter with a sigma value of 2, so it is
* as if we have a base scale of 2, and we start with half sigma as an initial guess. See
* Section 4.2 for more information */
double scale_factor = reference_sigma / 2.0;
const int maximum_interations = 10;
for (int i = 0; i < maximum_interations; i++) {
const double variance = compute_scaled_poles_variance(poles, scale_factor);
/* Close enough, we have found our scale factor. */
if (math::abs(reference_variance - variance) < 1.0e-7) {
return scale_factor;
}
/* Improve guess using Newton's method. Notice that Newton's method is a root finding method,
* so we supply the difference to the reference variance as our function, since the zero point
* will be when the variance is equal to the reference one. The derivative is not affected
* since the reference variance is a constant. */
const double derivative = compute_scaled_poles_variance_derivative(poles, scale_factor);
scale_factor -= (variance - reference_variance) / derivative;
}
/* The paper mentions that only a few iterations are needed, so if we didn't converge after
* maximum_interations, something is probably wrong. */
BLI_assert_unreachable();
return scale_factor;
}
/* The poles were computed for a Gaussian filter with a sigma value of 2, so scale them using
* Equation (19) in Van Vliet's paper to have the given sigma value. This involves finding the
* appropriate scale factor based on Equation (20), see Section 4.2 and the find_scale_factor
* method for more information. */
static std::array<std::complex<double>, 4> computed_scaled_poles(
const std::array<std::complex<double>, 4> &poles, float sigma)
{
const double scale_factor = find_scale_factor(poles, sigma);
std::array<std::complex<double>, 4> scaled_poles;
for (int i = 0; i < poles.size(); i++) {
const std::complex<double> &pole = poles[i];
const double magnitude = std::pow(std::abs(pole), 1.0 / scale_factor);
const double phase = std::arg(pole) / scale_factor;
scaled_poles[i] = std::polar(magnitude, phase);
}
return scaled_poles;
}
/* Compute the causal poles from the non causal ones. Since the Gaussian is a real even function,
* the causal poles are just the inverse of the non causal poles, as noted in Equation (2) in Van
* Vliet's paper. */
static std::array<std::complex<double>, 4> compute_causal_poles(
const std::array<std::complex<double>, 4> &non_causal_poles)
{
std::array<std::complex<double>, 4> causal_poles;
for (int i = 0; i < non_causal_poles.size(); i++) {
causal_poles[i] = 1.0 / non_causal_poles[i];
}
return causal_poles;
}
/* Computes the feedback coefficients from the given poles based on the equations in Equation (13)
* in Van Vliet's paper. See Section 3.2 for more information. */
static double4 compute_feedback_coefficients(const std::array<std::complex<double>, 4> &poles)
{
/* Compute the gain of the poles, which is the "b" at the end of Equation (13). */
std::complex<double> gain = std::complex<double>(1.0, 0.0);
for (const std::complex<double> &pole : poles) {
gain /= pole;
}
/* Compute the coefficients b4, b3, b2, and b1 based on the expressions b_N, b_N-1, b_N-2, and
* b_N-3 respectively in Equation (13). b4 and b3 are trivial, while b2 and b1 can be computed by
* drawing the following summation trees, where each path from the root to the leaf is multiplied
* and added:
*
* b2
* ____/|\____
* / | \
* i --> 2 3 4
* | / \ /|\
* j --> 1 1 2 1 2 3
*
* b1
* ___/ \___
* / \
* i --> 3 4
* | / \
* j --> 2 2 3
* | | / \
* k --> 1 1 1 2
*
* Notice that the values of i, j, and k are 1-index, so we need to subtract one when accessing
* the poles. */
const std::complex<double> b4 = gain;
const std::complex<double> b3 = -gain * (poles[0] + poles[1] + poles[2] + poles[3]);
const std::complex<double> b2 = gain * (poles[1] * poles[0] + poles[2] * poles[0] +
poles[2] * poles[1] + poles[3] * poles[0] +
poles[3] * poles[1] + poles[3] * poles[2]);
const std::complex<double> b1 = -gain * (poles[2] * poles[1] * poles[0] +
poles[3] * poles[1] * poles[0] +
poles[3] * poles[2] * poles[0] +
poles[3] * poles[2] * poles[1]);
/* The coefficients are actually real valued as guaranteed by Equations (10) and (2) since
* the poles are complex conjugate pairs. See Section 3.3 of the paper. */
const double4 coefficients = double4(b1.real(), b2.real(), b3.real(), b4.real());
return coefficients;
}
/* Computes the feedforward coefficient from the feedback coefficients based on Equation (12) of
* Van Vliet's paper. See Section 3.2 for more information. */
static double compute_feedforward_coefficient(const double4 &feedback_coefficients)
{
return 1.0 + math::reduce_add(feedback_coefficients);
}
/**
* Computes the residue of the partial fraction of the transfer function of the given causal poles
* and gain for the given target pole. This essentially evaluates Equation (3.41) in Oppenheim's
* book, where d_k is the target pole and assuming the transfer function is in the form given in
* Equation (3.39), where d_k are the poles. See the following derivation for the gain value.
*
* For the particular case of the Van Vliet's system, there are no zeros, so the numerator in
* Equation (3.39) is one. Further note that Van Vliet's formulation is different from the expected
* form, so we need to rearrange Equation (3) in to match the form in Equation (3.39), which is
* shown below.
*
* Start from the causal term of Equation (3):
*
* \code{.tex}
* H_+(z) = \prod_{i=1}^N \frac{d_i - 1}{d_i - z^{-1}}
* \endcode
*
* Divide by d_i:
*
* \code{.tex}
* H_+(z) = \prod_{i=1}^N \frac{1 - d_i^{-1}}{1 - d_i^{-1}z^{-1}}
* \endcode
*
* Move the numerator to its own product:
*
* \code{.tex}
* H_+(z) = \prod_{i=1}^N 1 - d_i^{-1} \prod_{i=1}^N \frac{1}{1 - d_i^{-1}z^{-1}}
* \endcode
*
* And we reach the same form as Equation (3.39). Where the first product term is b0 / a0 and is
* also the given gain value, which is also the same as the feedforward coefficient denoted by
* the alpha in Equation (12). Further d_i^{-1} in our derivation is the same as d_k in Equation
* (3.39), the discrepancy in the inverse operator is the fact that Van Vliet's derivation assume
* non causal poles, while Oppenheim's assume causal poles, which are inverse of each other as can
* be seen in the compute_causal_poles function.
*/
static std::complex<double> compute_partial_fraction_residue(
const std::array<std::complex<double>, 4> &poles,
const std::complex<double> &target_pole,
double gain)
{
/* Evaluating Equation (3.41) actually corresponds to omitting the terms in Equation (3.39) that
* corresponds to the target pole or its conjugate, because they get canceled by the first term
* in Equation (3.41). That's we are essentially evaluating the limit as the expression tends to
* the target pole. */
std::complex<double> target_pole_inverse = 1.0 / target_pole;
std::complex<double> residue = std::complex<double>(1.0, 0.0);
for (const std::complex<double> &pole : poles) {
if (pole != target_pole && pole != std::conj(target_pole)) {
residue *= 1.0 - pole * target_pole_inverse;
}
}
/* Remember that the gain is the b0 / a0 expression in Equation (3.39). */
return gain / residue;
}
/* Evaluates the causal transfer function at the reciprocal of the given pole, which will be the
* non causal pole if the given pole is a causal one, as discussed in the compute_causal_poles
* function. The causal transfer function is given in Equation (3) in Van Vliet's paper, but we
* compute it in the form derived in the description of the compute_partial_fraction_residue
* function, also see the aforementioned function for the gain value. */
static std::complex<double> compute_causal_transfer_function_at_non_causal_pole(
const std::array<std::complex<double>, 4> &poles,
const std::complex<double> &target_pole,
double gain)
{
std::complex<double> result = std::complex<double>(1.0, 0.0);
for (const std::complex<double> &pole : poles) {
result *= 1.0 - pole * target_pole;
}
return gain / result;
}
/* Combine each pole and its conjugate counterpart into a second order section and assign its
* coefficients to the given coefficients value. The residue of the pole and its transfer value in
* the partial fraction of its transfer function are given.
*
* TODO: Properly document this function and prove its equations. */
static void compute_second_order_section(const std::complex<double> &pole,
const std::complex<double> &residue,
const std::complex<double> &transfer_value,
double2 &r_feedback_coefficients,
double2 &r_causal_feedforward_coefficients,
double2 &r_non_causal_feedforward_coefficients)
{
const std::complex<double> parallel_residue = residue * transfer_value;
const std::complex<double> pole_inverse = 1.0 / pole;
r_feedback_coefficients = double2(-2.0 * pole.real(), std::norm(pole));
const double causal_feedforward_1 = parallel_residue.imag() / pole_inverse.imag();
const double causal_feedforward_0 = parallel_residue.real() -
causal_feedforward_1 * pole_inverse.real();
r_causal_feedforward_coefficients = double2(causal_feedforward_0, causal_feedforward_1);
const double non_causal_feedforward_1 = causal_feedforward_1 -
causal_feedforward_0 * r_feedback_coefficients[0];
const double non_causal_feedforward_2 = -causal_feedforward_0 * r_feedback_coefficients[1];
r_non_causal_feedforward_coefficients = double2(non_causal_feedforward_1,
non_causal_feedforward_2);
}
/**
* The IIR filter difference equation relies on previous outputs to compute new outputs, those
* previous outputs are not really defined at the start of the filter. To do Neumann boundary
* condition, we initialize the previous output with a special value that is a function of the
* boundary value. This special value is computed by multiply the boundary value with a coefficient
* to simulate an infinite stream of the boundary value.
*
* The function for the coefficient can be derived by substituting the boundary value for previous
* inputs, equating all current and previous outputs to the same value, and finally rearranging to
* compute that same output value.
*
* Start by the difference equation where b_i are the feedforward coefficients and a_i are the
* feedback coefficients:
*
* \code{.tex}
* y[n] = \sum_{i = 0}^3 b_i x[n - i] - \sum_{i = 0}^3 a_i y[n - i]
* \endcode
*
* Assume all outputs are y and all inputs are x, which is the boundary value:
*
* \code{.tex}
* y = \sum_{i = 0}^3 b_i x - \sum_{i = 0}^3 a_i y
* \endcode
*
* Now rearrange to compute y:
*
* \code{.tex}
* y = x \sum_{i = 0}^3 b_i - y \sum_{i = 0}^3 a_i
* y + y \sum_{i = 0}^3 a_i = x \sum_{i = 0}^3 b_i
* y (1 + \sum_{i = 0}^3 a_i) = x \sum_{i = 0}^3 b_i
* y = x \cdot \frac{\sum_{i = 0}^3 b_i}{1 + \sum_{i = 0}^3 a_i}
* \endcode
*
* So our coefficient is the value that is multiplied by the boundary value x. Had x been zero,
* that is, we are doing Dirichlet boundary condition, the equations still hold.
*/
static double compute_boundary_coefficient(const double2 &feedback_coefficients,
const double2 &feedforward_coefficients)
{
return math::reduce_add(feedforward_coefficients) /
(1.0 + math::reduce_add(feedback_coefficients));
}
/* Computes the feedback and feedforward coefficients for the 4th order Van Vliet Gaussian filter
* given a target Gaussian sigma value. We first scale the poles of the filter to match the sigma
* value based on the method described in Section 4.2 of Van Vliet's paper, then we compute the
* coefficients from the scaled poles based on Equations (12) and (13). */
VanVlietGaussianCoefficients::VanVlietGaussianCoefficients(Context & /*context*/, float sigma)
{
/* The 4th order (N=4) poles for the Gaussian filter of a sigma of 2 computed by minimizing the
* maximum error (L-infinity) to true Gaussian as provided in Van Vliet's paper Table (1) fourth
* column. Notice that the second and fourth poles are the complex conjugates of the first and
* third poles respectively as noted in the table description. */
const std::array<std::complex<double>, 4> poles = {
std::complex<double>(1.12075, 1.27788),
std::complex<double>(1.12075, -1.27788),
std::complex<double>(1.76952, 0.46611),
std::complex<double>(1.76952, -0.46611),
};
const std::array<std::complex<double>, 4> scaled_poles = computed_scaled_poles(poles, sigma);
/* The given poles are actually the non causal poles, since they are outside of the unit circle,
* as demonstrated in Section 3.4 of Van Vliet's paper. And we compute the causal poles from
* those. */
const std::array<std::complex<double>, 4> non_causal_poles = scaled_poles;
const std::array<std::complex<double>, 4> causal_poles = compute_causal_poles(non_causal_poles);
/* Compute the feedforward and feedback coefficients, noting that those are functions of the non
* causal poles. */
const double4 feedback_coefficients = compute_feedback_coefficients(non_causal_poles);
const double feedforward_coefficient = compute_feedforward_coefficient(feedback_coefficients);
/* We only compute the residue for two of the causal poles, since the other two are complex
* conjugates of those two, and their residues will also be the complex conjugate of their
* respective counterpart. The gain is the feedforward coefficient as discussed in the function
* description. */
const std::complex<double> first_residue = compute_partial_fraction_residue(
causal_poles, causal_poles[0], feedforward_coefficient);
const std::complex<double> second_residue = compute_partial_fraction_residue(
causal_poles, causal_poles[2], feedforward_coefficient);
/* We only compute the transfer value of for two of the non causal poles, since the other two are
* complex conjugates of those two, and their transfer values will also be the complex conjugate
* of their respective counterpart. The gain is the feedforward coefficient as discussed in the
* function description. */
const std::complex<double> first_transfer_value =
compute_causal_transfer_function_at_non_causal_pole(
causal_poles, causal_poles[0], feedforward_coefficient);
const std::complex<double> second_transfer_value =
compute_causal_transfer_function_at_non_causal_pole(
causal_poles, causal_poles[2], feedforward_coefficient);
/* Combine each pole and its conjugate counterpart into a second order section and assign its
* coefficients. */
compute_second_order_section(causal_poles[0],
first_residue,
first_transfer_value,
first_feedback_coefficients_,
first_causal_feedforward_coefficients_,
first_non_causal_feedforward_coefficients_);
compute_second_order_section(causal_poles[2],
second_residue,
second_transfer_value,
second_feedback_coefficients_,
second_causal_feedforward_coefficients_,
second_non_causal_feedforward_coefficients_);
/* Compute the boundary coefficients for all four of second order sections. */
first_causal_boundary_coefficient_ = compute_boundary_coefficient(
first_feedback_coefficients_, first_causal_feedforward_coefficients_);
first_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
first_feedback_coefficients_, first_non_causal_feedforward_coefficients_);
second_causal_boundary_coefficient_ = compute_boundary_coefficient(
second_feedback_coefficients_, second_causal_feedforward_coefficients_);
second_non_causal_boundary_coefficient_ = compute_boundary_coefficient(
second_feedback_coefficients_, second_non_causal_feedforward_coefficients_);
}
const double2 &VanVlietGaussianCoefficients::first_causal_feedforward_coefficients() const
{
return first_causal_feedforward_coefficients_;
}
const double2 &VanVlietGaussianCoefficients::first_non_causal_feedforward_coefficients() const
{
return first_non_causal_feedforward_coefficients_;
}
const double2 &VanVlietGaussianCoefficients::first_feedback_coefficients() const
{
return first_feedback_coefficients_;
}
const double2 &VanVlietGaussianCoefficients::second_causal_feedforward_coefficients() const
{
return second_causal_feedforward_coefficients_;
}
const double2 &VanVlietGaussianCoefficients::second_non_causal_feedforward_coefficients() const
{
return second_non_causal_feedforward_coefficients_;
}
const double2 &VanVlietGaussianCoefficients::second_feedback_coefficients() const
{
return second_feedback_coefficients_;
}
double VanVlietGaussianCoefficients::first_causal_boundary_coefficient() const
{
return first_causal_boundary_coefficient_;
}
double VanVlietGaussianCoefficients::first_non_causal_boundary_coefficient() const
{
return first_non_causal_boundary_coefficient_;
}
double VanVlietGaussianCoefficients::second_causal_boundary_coefficient() const
{
return second_causal_boundary_coefficient_;
}
double VanVlietGaussianCoefficients::second_non_causal_boundary_coefficient() const
{
return second_non_causal_boundary_coefficient_;
}
/* --------------------------------------------------------------------
* Van Vliet Gaussian Coefficients Container.
*/
void VanVlietGaussianCoefficientsContainer::reset()
{
/* First, delete all resources that are no longer needed. */
map_.remove_if([](auto item) { return !item.value->needed; });
/* Second, reset the needed status of the remaining resources to false to ready them to track
* their needed status for the next evaluation. */
for (auto &value : map_.values()) {
value->needed = false;
}
}
VanVlietGaussianCoefficients &VanVlietGaussianCoefficientsContainer::get(Context &context,
float sigma)
{
const VanVlietGaussianCoefficientsKey key(sigma);
auto &deriche_gaussian_coefficients = *map_.lookup_or_add_cb(
key, [&]() { return std::make_unique<VanVlietGaussianCoefficients>(context, sigma); });
deriche_gaussian_coefficients.needed = true;
return deriche_gaussian_coefficients;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,91 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#ifdef WITH_OPENIMAGEDENOISE
# include <cstdint>
# include <memory>
# include <string>
# include "BLI_map.hh"
# include <OpenImageDenoise/oidn.hpp>
# include "COM_result.hh"
namespace blender::compositor {
class Context;
class Result;
enum class DenoisedAuxiliaryPassType : uint8_t {
Albedo,
Normal,
};
/* ------------------------------------------------------------------------------------------------
* Denoised Auxiliary Pass Key.
*/
class DenoisedAuxiliaryPassKey {
public:
DenoisedAuxiliaryPassType type;
oidn::Quality quality;
DenoisedAuxiliaryPassKey(const DenoisedAuxiliaryPassType type, const oidn::Quality quality);
uint64_t hash() const;
};
bool operator==(const DenoisedAuxiliaryPassKey &a, const DenoisedAuxiliaryPassKey &b);
/* -------------------------------------------------------------------------------------------------
* Denoised Auxiliary Pass.
*
* A derived result that stores a denoised version of the auxiliary pass of the given type using
* the given quality. */
class DenoisedAuxiliaryPass {
public:
Result result;
public:
DenoisedAuxiliaryPass(Context &context,
const Result &pass,
const DenoisedAuxiliaryPassType type,
const oidn::Quality quality);
~DenoisedAuxiliaryPass();
};
/* ------------------------------------------------------------------------------------------------
* Denoised Auxiliary Pass Container.
*/
class DenoisedAuxiliaryPassContainer {
private:
Map<DenoisedAuxiliaryPassKey, std::unique_ptr<DenoisedAuxiliaryPass>> map_;
public:
/* Check if there is an available DenoisedAuxiliaryPass derived resource with the given
* parameters in the container, if one exists, return it, otherwise, return a newly created one
* and add it to the container. */
DenoisedAuxiliaryPass &get(Context &context,
const Result &pass,
const DenoisedAuxiliaryPassType type,
const oidn::Quality quality);
};
} // namespace blender::compositor
#else
namespace blender::compositor {
/* Building without OIDN, define a dummy container. User is not expected to use it if OIDN is not
* available. */
class DenoisedAuxiliaryPassContainer {};
} // namespace blender::compositor
#endif

View File

@@ -0,0 +1,146 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#ifdef WITH_OPENIMAGEDENOISE
# include <cstdint>
# include <memory>
# include "BLI_assert.h"
# include "BLI_hash.hh"
# include "COM_context.hh"
# include "COM_denoised_auxiliary_pass.hh"
# include "COM_result.hh"
# include "COM_utilities.hh"
# include "COM_utilities_oidn.hh"
# include <OpenImageDenoise/oidn.hpp>
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Denoised Auxiliary Pass Key.
*/
DenoisedAuxiliaryPassKey::DenoisedAuxiliaryPassKey(const DenoisedAuxiliaryPassType type,
const oidn::Quality quality)
: type(type), quality(quality)
{
}
uint64_t DenoisedAuxiliaryPassKey::hash() const
{
return get_default_hash(this->type, this->quality);
}
bool operator==(const DenoisedAuxiliaryPassKey &a, const DenoisedAuxiliaryPassKey &b)
{
return a.type == b.type && a.quality == b.quality;
}
/* --------------------------------------------------------------------
* Denoised Auxiliary Pass.
*/
/* A callback to cancel the filter operations by evaluating the context's is_canceled method. The
* API specifies that true indicates the filter should continue, while false indicates it should
* stop, so invert the condition. This callback can also be used to track progress using the given
* n argument, but we currently don't make use of it. See OIDNProgressMonitorFunction in the API
* for more information. */
static bool oidn_progress_monitor_function(void *user_ptr, double /*n*/)
{
const Context *context = static_cast<const Context *>(user_ptr);
return !context->is_canceled();
}
static const char *get_pass_name(const DenoisedAuxiliaryPassType type)
{
switch (type) {
case DenoisedAuxiliaryPassType::Albedo:
return "albedo";
case DenoisedAuxiliaryPassType::Normal:
return "normal";
}
BLI_assert_unreachable();
return "";
}
DenoisedAuxiliaryPass::DenoisedAuxiliaryPass(Context &context,
const Result &pass,
const DenoisedAuxiliaryPassType type,
const oidn::Quality quality)
: result(context.create_result(pass.type()))
{
Result denoise_input = context.create_result(pass.type());
Result *denoise_output = nullptr;
bool is_denoising_in_place = false;
if (context.use_gpu()) {
Result input_pass_cpu = pass.download_to_cpu();
denoise_input.share_data(input_pass_cpu);
input_pass_cpu.release();
denoise_output = &denoise_input;
is_denoising_in_place = true;
}
else {
denoise_input.share_data(pass);
this->result.allocate_texture(pass.domain());
denoise_output = &this->result;
}
oidn::DeviceRef device = create_oidn_device(context);
device.commit();
oidn::BufferRef input_buffer = create_oidn_buffer(device, denoise_input);
oidn::BufferRef output_buffer = create_oidn_buffer(device, *denoise_output);
oidn::FilterRef filter = device.newFilter("RT");
const char *pass_name = get_pass_name(type);
const int2 size = pass.domain().data_size;
const int pixel_stride = sizeof(float) * denoise_input.channels_count();
filter.setImage(pass_name, input_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
filter.setImage("output", output_buffer, oidn::Format::Float3, size.x, size.y, 0, pixel_stride);
filter.set("quality", quality);
filter.setProgressMonitorFunction(oidn_progress_monitor_function, &context);
filter.commit();
filter.execute();
if (output_buffer.getStorage() != oidn::Storage::Host) {
output_buffer.read(
0, denoise_output->size_in_bytes(), denoise_output->cpu_data_for_write().data());
}
if (is_denoising_in_place) {
this->result.share_data(denoise_input);
}
denoise_input.release();
}
DenoisedAuxiliaryPass::~DenoisedAuxiliaryPass()
{
this->result.release();
}
/* --------------------------------------------------------------------
* Denoised Auxiliary Pass Container.
*/
DenoisedAuxiliaryPass &DenoisedAuxiliaryPassContainer::get(Context &context,
const Result &pass,
const DenoisedAuxiliaryPassType type,
const oidn::Quality quality)
{
const DenoisedAuxiliaryPassKey key(type, quality);
return *map_.lookup_or_add_cb(key, [&]() {
return std::make_unique<DenoisedAuxiliaryPass>(context, pass, type, quality);
});
}
} // namespace blender::compositor
#endif

View File

@@ -0,0 +1,381 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_compile_state.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_implicit_input_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_node_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_shader_operation.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
CompileState::CompileState(const Context &context, const Schedule &schedule)
: context_(context), schedule_(schedule)
{
}
const Schedule &CompileState::get_schedule()
{
return schedule_;
}
void CompileState::map_node_to_node_operation(const bNode &node, NodeOperation *operations)
{
node_operations_.add_new(&node, operations);
}
void CompileState::map_node_to_pixel_operation(const bNode &node, PixelOperation *operations)
{
pixel_operations_.add_new(&node, operations);
}
Result &CompileState::get_result_from_output_socket(const bNodeSocket &output)
{
/* The output belongs to a node that was compiled into a standard node operation, so return a
* reference to the result from that operation using the output identifier. */
if (node_operations_.contains(&output.owner_node())) {
NodeOperation *operation = node_operations_.lookup(&output.owner_node());
return operation->get_result(output.identifier);
}
/* Otherwise, the output belongs to a node that was compiled into a pixel operation, so retrieve
* the internal identifier of that output and return a reference to the result from that
* operation using the retrieved identifier. */
PixelOperation *operation = pixel_operations_.lookup(&output.owner_node());
return operation->get_result(operation->get_output_identifier_from_output_socket(output));
}
void CompileState::add_node_to_pixel_compile_unit(const bNode &node)
{
pixel_compile_unit_.add_new(&node);
/* If this is the first node in the compile unit, then we should initialize the single value
* type, as well as the domain in case the node was not single value. */
const bool is_first_node_in_operation = pixel_compile_unit_.size() == 1;
if (is_first_node_in_operation) {
is_pixel_compile_unit_single_value_ = this->is_pixel_node_single_value(node);
/* If the node was not a single value, compute and initialize the domain. */
if (!is_pixel_compile_unit_single_value_) {
pixel_compile_unit_domain_ = this->compute_pixel_node_domain(node);
}
}
}
PixelCompileUnit &CompileState::get_pixel_compile_unit()
{
return pixel_compile_unit_;
}
bool CompileState::is_pixel_compile_unit_single_value()
{
return is_pixel_compile_unit_single_value_;
}
void CompileState::reset_pixel_compile_unit()
{
pixel_compile_unit_.clear();
pixel_compile_unit_domain_.reset();
}
bool CompileState::should_compile_pixel_compile_unit(const bNode &node)
{
/* If the pixel compile unit is empty, then it can't be compiled yet. */
if (pixel_compile_unit_.is_empty()) {
return false;
}
/* If the node is not a pixel node, then it can't be added to the pixel compile unit and the
* pixel compile unit is considered complete and should be compiled. */
if (!is_pixel_node(node)) {
return true;
}
/* If the compile unit is single value and the given node is not or vice versa, then it can't be
* added to the pixel compile unit and the pixel compile unit is considered complete and should
* be compiled. */
if (is_pixel_compile_unit_single_value_ != this->is_pixel_node_single_value(node)) {
return true;
}
/* For non single value compile units, if the computed domain of the node doesn't matches the
* domain of the pixel compile unit, then it can't be added to the pixel compile unit and the
* pixel compile unit is considered complete and should be compiled. */
if (!is_pixel_compile_unit_single_value_) {
if (pixel_compile_unit_domain_.value() != this->compute_pixel_node_domain(node)) {
return true;
}
}
/* Otherwise, the node is compatible and can be added to the compile unit and it shouldn't be
* compiled just yet. */
return false;
}
bool CompileState::is_pixel_node_single_value(const bNode &node)
{
/* If any of the outputs are single-only outputs, then the node is operating on single values. */
for (const bNodeSocket *output : node.output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
if (Result::is_single_value_only_type(get_node_socket_result_type(output))) {
return true;
}
}
/* If any of the inputs are single-only outputs, then the node is operating on single values. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
if (Result::is_single_value_only_type(get_node_socket_result_type(input))) {
return true;
}
}
/* The pixel node is single value when all of its inputs are single values. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
/* The input does not have an implicit input, so it is a single value. */
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
const std::optional<Domain> domain = ImplicitInputOperation::get_domain(
context_, input_descriptor.implicit_input.value());
if (!domain.has_value()) {
/* The input has an implicit input, but it is a single value. */
continue;
}
/* Otherwise, it has an non-single-value implicit input. */
return false;
}
/* If the output belongs to a node that is part of the pixel compile unit and that compile unit
* is not single value, then the node is not single value. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
if (is_pixel_compile_unit_single_value_) {
continue;
}
return false;
}
const Result &result = get_result_from_output_socket(*output);
if (!result.is_single_value()) {
return false;
}
}
return true;
}
Domain CompileState::compute_pixel_node_domain(const bNode &node)
{
/* Default to an identity domain in case no domain input was found, most likely because all
* inputs are single values. */
Domain node_domain = Domain::identity();
int current_domain_priority = std::numeric_limits<int>::max();
/* Go over the inputs and find the domain of the non single value input with the highest domain
* priority. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
/* The input does not have an implicit input, so it is a single that can't be a domain input
* and we skip it. */
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
const std::optional<Domain> domain = ImplicitInputOperation::get_domain(
context_, input_descriptor.implicit_input.value());
if (!domain.has_value()) {
/* The input has an implicit input, but it is a single value that can't be a domain input
* and we skip it. */
continue;
}
/* Otherwise, the input has the domain of the implicit input, which is the domain of the
* compositing region. Notice that the lower the domain priority value is, the higher the
* priority is, hence the less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = domain.value();
current_domain_priority = input_descriptor.domain_priority;
}
continue;
}
/* If the output belongs to a node that is part of the pixel compile unit, then the domain of
* the input is the domain of the compile unit itself. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
/* Notice that the lower the domain priority value is, the higher the priority is, hence the
* less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = pixel_compile_unit_domain_.value();
current_domain_priority = input_descriptor.domain_priority;
}
continue;
}
const Result &result = get_result_from_output_socket(*output);
/* A single value input can't be a domain input. */
if (result.is_single_value() || input_descriptor.expects_single_value) {
continue;
}
/* An input that skips operation domain realization can't be a domain input. */
if (input_descriptor.realization_mode != InputRealizationMode::OperationDomain) {
continue;
}
/* Notice that the lower the domain priority value is, the higher the priority is, hence the
* less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = result.domain();
current_domain_priority = input_descriptor.domain_priority;
}
}
return node_domain;
}
bool CompileState::pixel_compile_unit_has_too_many_outputs(const bool are_node_previews_needed)
{
/* Only GPU and non-single units have output count limitations. */
if (!context_.use_gpu() || is_pixel_compile_unit_single_value_) {
return false;
}
int outputs_count = 0;
for (const bNode *node : pixel_compile_unit_) {
const bNodeSocket *preview_output = are_node_previews_needed ?
find_preview_output_socket(*node) :
nullptr;
for (const bNodeSocket *output : node->output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
/* If the output is used as the node preview, then an operation output will exist for it. */
const bool is_preview_output = output == preview_output;
/* If any of the nodes linked to the output are not part of the pixel compile unit but are
* part of the execution schedule, then an operation output will exist for it. */
const bool is_operation_output = is_output_linked_to_input_conditioned(
*output, [&](const bNodeSocket &input) {
return schedule_.nodes.contains(&input.owner_node()) &&
!schedule_.unneeded_inputs.contains(&input) &&
!pixel_compile_unit_.contains(&input.owner_node());
});
if (is_operation_output || is_preview_output) {
outputs_count += 1;
}
if (outputs_count > ShaderOperation::maximum_outputs_count) {
return true;
}
}
}
return false;
}
bool CompileState::pixel_compile_unit_has_too_many_inputs()
{
/* Only GPU and non-single units have input count limitations. */
if (!context_.use_gpu() || is_pixel_compile_unit_single_value_) {
return false;
}
Set<ImplicitInputType> referenced_implicit_inputs;
Set<const bNodeSocket *> referenced_output_sockets;
int inputs_count = 0;
for (const bNode *node : pixel_compile_unit_) {
for (const bNodeSocket *input : node->input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
/* All implicit inputs of the same type share the same input, and this one was counted
* before, so no need to count it again. */
if (referenced_implicit_inputs.contains(input_descriptor.implicit_input.value())) {
continue;
}
inputs_count++;
if (inputs_count > ShaderOperation::maximum_inputs_count) {
return true;
}
referenced_implicit_inputs.add_new(input_descriptor.implicit_input.value());
continue;
}
/* This output is part of the pixel compile unit, so no input is declared for it. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
continue;
}
/* All inputs linked to the same output share the same input, and this one was counted
* before, so no need to count it again. */
if (referenced_output_sockets.contains(output)) {
continue;
}
inputs_count++;
if (inputs_count > ShaderOperation::maximum_inputs_count) {
return true;
}
referenced_output_sockets.add_new(output);
}
}
return false;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,129 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_render_context.hh"
#include "COM_static_cache_manager.hh"
namespace blender::compositor {
Context::Context(StaticCacheManager &cache_manager) : cache_manager_(cache_manager) {};
Result Context::get_pass(const Scene * /*scene*/, int /*view_layer*/, const char * /*name*/)
{
compositor::Result invalid_pass = this->create_result(compositor::ResultType::Color);
invalid_pass.allocate_invalid();
return invalid_pass;
}
const RenderData &Context::get_render_data() const
{
return this->get_scene().r;
}
StringRef Context::get_view_name() const
{
return "";
}
ResultPrecision Context::get_precision() const
{
return ResultPrecision::Full;
}
void Context::set_info_message(StringRef /*message*/) const {}
bool Context::treat_viewer_as_group_output() const
{
return false;
}
void Context::populate_meta_data_for_pass(const Scene * /*scene*/,
int /*view_layer_id*/,
const char * /*pass_name*/,
MetaData & /*meta_data*/) const
{
}
RenderContext *Context::render_context() const
{
return nullptr;
}
nodes::eval_log::NodesEvalLog *Context::nodes_evaluation_log() const
{
return nullptr;
}
void Context::evaluate_operation_post() const {}
bool Context::is_canceled() const
{
return false;
}
float Context::get_render_percentage() const
{
return get_render_data().size / 100.0f;
}
int Context::get_frame_number() const
{
return get_render_data().cfra;
}
float Context::get_time() const
{
const float frame_number = float(get_frame_number());
const float frame_rate = float(get_render_data().frs_sec) /
float(get_render_data().frs_sec_base);
return frame_number / frame_rate;
}
eCompositorDenoiseQaulity Context::get_denoise_quality() const
{
if (this->render_context()) {
return static_cast<eCompositorDenoiseQaulity>(
this->get_render_data().compositor_denoise_final_quality);
}
return static_cast<eCompositorDenoiseQaulity>(
this->get_render_data().compositor_denoise_preview_quality);
}
gpu::Shader *Context::get_shader(const char *info_name, ResultPrecision precision)
{
return cache_manager().cached_shaders.get(info_name, precision);
}
gpu::Shader *Context::get_shader(const char *info_name)
{
return get_shader(info_name, get_precision());
}
Result Context::create_result(ResultType type, ResultPrecision precision)
{
return Result(*this, type, precision);
}
Result Context::create_result(ResultType type)
{
return create_result(type, get_precision());
}
StaticCacheManager &Context::cache_manager()
{
return cache_manager_;
}
const Strip *Context::get_strip() const
{
return nullptr;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/format.h>
#include "BLI_generic_span.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_utildefines.h"
#include "GPU_shader.hh"
#include "IMB_colormanagement.hh"
#include "BKE_type_conversions.hh"
#include "COM_context.hh"
#include "COM_conversion_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
ConversionOperation::ConversionOperation(Context &context,
const ResultType input_type,
const ResultType expected_type)
: SimpleOperation(context)
{
this->declare_input_descriptor(InputDescriptor{input_type});
this->populate_result(expected_type);
}
void ConversionOperation::execute()
{
Result &result = this->get_result();
const Result &input = this->get_input();
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
if (!conversions.is_convertible(input.get_cpp_type(), result.get_cpp_type())) {
this->allocate_default_remaining_outputs();
return;
}
if (input.is_single_value()) {
result.allocate_single_value();
this->execute_single(input, result);
return;
}
result.allocate_texture(input.domain());
if (this->context().use_gpu()) {
const std::string shader_name = fmt::format("compositor_convert_{}_to_{}",
Result::type_name(this->get_input().type()),
Result::type_name(this->get_result().type()));
gpu::Shader *shader = this->context().get_shader(shader_name.c_str());
GPU_shader_bind(shader);
if (this->get_input().type() == ResultType::Color &&
ELEM(this->get_result().type(), ResultType::Float, ResultType::Int, ResultType::Bool))
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
GPU_shader_uniform_3fv(shader, "luminance_coefficients_u", luminance_coefficients);
}
input.bind_as_texture(shader, "input_tx");
result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
input.unbind_as_texture();
result.unbind_as_image();
GPU_shader_unbind();
}
else {
this->execute_cpu(input, result);
}
}
SimpleOperation *ConversionOperation::construct_if_needed(Context &context,
const Result &input_result,
const InputDescriptor &input_descriptor)
{
if (input_descriptor.skip_type_conversion) {
return nullptr;
}
const ResultType result_type = input_result.type();
const ResultType expected_type = input_descriptor.type;
if (result_type != expected_type) {
return new ConversionOperation(context, result_type, expected_type);
}
return nullptr;
}
void ConversionOperation::execute_single(const Result &input, Result &output)
{
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
conversions.convert_to_initialized_n(
GSpan(input.single_value().type(), input.single_value().get(), 1),
GMutableSpan(output.single_value().type(), output.single_value().get(), 1));
output.update_single_value_data();
}
void ConversionOperation::execute_cpu(const Result &input, Result &output)
{
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
conversions.convert_to_initialized_n(input.cpu_data(), output.cpu_data_for_write());
}
} // namespace blender::compositor

Some files were not shown because too many files have changed in this diff Show More