diff --git a/source/blender/compositor/CMakeLists.txt b/source/blender/compositor/CMakeLists.txt index 447779c5996..cdad91ee758 100644 --- a/source/blender/compositor/CMakeLists.txt +++ b/source/blender/compositor/CMakeLists.txt @@ -6,6 +6,7 @@ set(INC . algorithms cached_resources + derived_resources utilities ../gpu/intern ../makesrna @@ -21,6 +22,7 @@ set(SRC COM_compositor.hh COM_context.hh COM_conversion_operation.hh + COM_derived_resources.hh COM_domain.hh COM_evaluator.hh COM_input_descriptor.hh @@ -133,6 +135,10 @@ set(SRC 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/COM_utilities_diagonals.hh utilities/COM_utilities_type_conversion.hh ) diff --git a/source/blender/compositor/COM_derived_resources.hh b/source/blender/compositor/COM_derived_resources.hh new file mode 100644 index 00000000000..fb1ecf6b6ed --- /dev/null +++ b/source/blender/compositor/COM_derived_resources.hh @@ -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 diff --git a/source/blender/compositor/COM_result.hh b/source/blender/compositor/COM_result.hh index 90ace1d846e..cff83fcc445 100644 --- a/source/blender/compositor/COM_result.hh +++ b/source/blender/compositor/COM_result.hh @@ -18,6 +18,7 @@ #include "GPU_shader.hh" #include "GPU_texture.hh" +#include "COM_derived_resources.hh" #include "COM_domain.hh" #include "COM_meta_data.hh" @@ -97,7 +98,11 @@ enum class ResultStorageType : uint8_t { * * A result can wrap an external texture that is not allocated nor managed by the result. This is * set up by a call to the wrap_external method. In that case, when the reference count eventually - * reach zero, the texture will not be freed. */ + * reach zero, the texture will not be freed. + * + * 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 @@ -163,6 +168,9 @@ class Result { * context and should be released back into the pool instead of being freed. For CPU storage, * this is irrelevant. */ bool is_from_pool_ = false; + /* 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 @@ -332,6 +340,10 @@ class Result { * 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; diff --git a/source/blender/compositor/derived_resources/COM_denoised_auxiliary_pass.hh b/source/blender/compositor/derived_resources/COM_denoised_auxiliary_pass.hh new file mode 100644 index 00000000000..b1a483ec9e9 --- /dev/null +++ b/source/blender/compositor/derived_resources/COM_denoised_auxiliary_pass.hh @@ -0,0 +1,89 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#ifdef WITH_OPENIMAGEDENOISE + +# include +# include +# include + +# include "BLI_map.hh" + +# include + +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: + float *denoised_buffer = nullptr; + + public: + DenoisedAuxiliaryPass(Context &context, + const Result &pass, + const DenoisedAuxiliaryPassType type, + const oidn::Quality quality); + + ~DenoisedAuxiliaryPass(); +}; + +/* ------------------------------------------------------------------------------------------------ + * Denoised Auxiliary Pass Container. + */ +class DenoisedAuxiliaryPassContainer { + private: + Map> 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 diff --git a/source/blender/compositor/derived_resources/intern/denoised_auxiliary_pass.cc b/source/blender/compositor/derived_resources/intern/denoised_auxiliary_pass.cc new file mode 100644 index 00000000000..8d7049171d6 --- /dev/null +++ b/source/blender/compositor/derived_resources/intern/denoised_auxiliary_pass.cc @@ -0,0 +1,133 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#ifdef WITH_OPENIMAGEDENOISE + +# include +# include + +# include "BLI_assert.h" +# include "BLI_hash.hh" + +# include "MEM_guardedalloc.h" + +# include "GPU_texture.hh" + +# include "COM_context.hh" +# include "COM_denoised_auxiliary_pass.hh" +# include "COM_result.hh" + +# include + +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(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) +{ + /* Assign the pass data to the denoised buffer since we will be denoising in place. */ + if (context.use_gpu()) { + GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE); + this->denoised_buffer = static_cast(GPU_texture_read(pass, GPU_DATA_FLOAT, 0)); + } + else { + this->denoised_buffer = static_cast(MEM_dupallocN(pass.float_texture())); + } + + const int width = pass.domain().size.x; + const int height = pass.domain().size.y; + const int pixel_stride = sizeof(float) * 4; + + oidn::DeviceRef device = oidn::newDevice(oidn::DeviceType::CPU); + device.commit(); + + /* Denoise the pass in place, so set it to both the input and output. */ + oidn::FilterRef filter = device.newFilter("RT"); + filter.setImage(get_pass_name(type), + this->denoised_buffer, + oidn::Format::Float3, + width, + height, + 0, + pixel_stride); + filter.setImage( + "output", this->denoised_buffer, oidn::Format::Float3, width, height, 0, pixel_stride); + filter.set("quality", quality); + filter.setProgressMonitorFunction(oidn_progress_monitor_function, &context); + filter.commit(); + filter.execute(); +} + +DenoisedAuxiliaryPass::~DenoisedAuxiliaryPass() +{ + MEM_freeN(this->denoised_buffer); +} + +/* -------------------------------------------------------------------- + * 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(context, pass, type, quality); + }); +} + +} // namespace blender::compositor + +#endif diff --git a/source/blender/compositor/intern/result.cc b/source/blender/compositor/intern/result.cc index 587b1a1ba5f..60382ce18a7 100644 --- a/source/blender/compositor/intern/result.cc +++ b/source/blender/compositor/intern/result.cc @@ -484,6 +484,9 @@ void Result::free() integer_texture_ = nullptr; break; } + + delete derived_resources_; + derived_resources_ = nullptr; } bool Result::should_compute() @@ -491,6 +494,14 @@ bool Result::should_compute() return initial_reference_count_ != 0; } +DerivedResources &Result::derived_resources() +{ + if (!derived_resources_) { + derived_resources_ = new DerivedResources(); + } + return *derived_resources_; +} + ResultType Result::type() const { return type_; diff --git a/source/blender/draw/CMakeLists.txt b/source/blender/draw/CMakeLists.txt index adacd7a6dcb..58312778de3 100644 --- a/source/blender/draw/CMakeLists.txt +++ b/source/blender/draw/CMakeLists.txt @@ -14,6 +14,7 @@ set(INC ../compositor ../compositor/algorithms ../compositor/cached_resources + ../compositor/derived_resources ../../../intern/opensubdiv ) diff --git a/source/blender/nodes/composite/CMakeLists.txt b/source/blender/nodes/composite/CMakeLists.txt index d3bd3f0e82e..a20df43ccea 100644 --- a/source/blender/nodes/composite/CMakeLists.txt +++ b/source/blender/nodes/composite/CMakeLists.txt @@ -11,6 +11,7 @@ set(INC ../../makesrna ../../compositor/algorithms ../../compositor/cached_resources + ../../compositor/derived_resources ../../compositor/utilities # RNA_prototypes.hh diff --git a/source/blender/nodes/composite/nodes/node_composite_denoise.cc b/source/blender/nodes/composite/nodes/node_composite_denoise.cc index e8564488d73..5a28b78d6aa 100644 --- a/source/blender/nodes/composite/nodes/node_composite_denoise.cc +++ b/source/blender/nodes/composite/nodes/node_composite_denoise.cc @@ -18,6 +18,7 @@ #include "DNA_node_types.h" +#include "COM_denoised_auxiliary_pass.hh" #include "COM_node_operation.hh" #include "COM_utilities.hh" @@ -131,14 +132,17 @@ class DenoiseOperation : public NodeOperation { const int pixel_stride = sizeof(float) * 4; const eGPUDataFormat data_format = GPU_DATA_FLOAT; + Vector temporary_buffers_to_free; + float *input_color = nullptr; float *output_color = nullptr; if (this->context().use_gpu()) { /* Download the input texture and set it as both the input and output of the filter to - * denoise it in-place. */ + * denoise it in-place. Make sure to track the downloaded buffer to be later freed. */ GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE); input_color = static_cast(GPU_texture_read(input_image, data_format, 0)); output_color = input_color; + temporary_buffers_to_free.append(input_color); } else { input_color = input_image.float_texture(); @@ -152,57 +156,56 @@ class DenoiseOperation : public NodeOperation { this->set_filter_quality(filter); filter.setProgressMonitorFunction(oidn_progress_monitor_function, &context()); - /* If the albedo input is not a single value input, download the albedo texture, denoise it - * in-place if denoising auxiliary passes is needed, and set it to the main filter. */ - float *albedo = nullptr; - Result &input_albedo = get_input("Albedo"); + /* If the albedo input is not a single value input, set it to the albedo input of the filter, + * denoising it if needed. */ + Result &input_albedo = this->get_input("Albedo"); if (!input_albedo.is_single_value()) { - if (this->context().use_gpu()) { - albedo = static_cast(GPU_texture_read(input_albedo, data_format, 0)); + float *albedo = nullptr; + if (this->should_denoise_auxiliary_passes()) { + albedo = input_albedo.derived_resources() + .denoised_auxiliary_passes + .get(this->context(), + input_albedo, + DenoisedAuxiliaryPassType::Albedo, + this->get_quality()) + .denoised_buffer; } else { - albedo = input_albedo.float_texture(); - } - - if (should_denoise_auxiliary_passes()) { - oidn::FilterRef albedoFilter = device.newFilter("RT"); - this->set_filter_quality(albedoFilter); - albedoFilter.setImage( - "albedo", albedo, oidn::Format::Float3, width, height, 0, pixel_stride); - albedoFilter.setImage( - "output", albedo, oidn::Format::Float3, width, height, 0, pixel_stride); - albedoFilter.setProgressMonitorFunction(oidn_progress_monitor_function, &context()); - albedoFilter.commit(); - albedoFilter.execute(); + if (this->context().use_gpu()) { + albedo = static_cast(GPU_texture_read(input_albedo, data_format, 0)); + temporary_buffers_to_free.append(albedo); + } + else { + albedo = input_albedo.float_texture(); + } } filter.setImage("albedo", albedo, oidn::Format::Float3, width, height, 0, pixel_stride); } - /* If the albedo and normal inputs are not single value inputs, download the normal texture, - * denoise it in-place if denoising auxiliary passes is needed, and set it to the main filter. - * Notice that we also consider the albedo input because OIDN doesn't support denoising with - * only the normal auxiliary pass. */ - float *normal = nullptr; - Result &input_normal = get_input("Normal"); - if (albedo && !input_normal.is_single_value()) { - if (this->context().use_gpu()) { - normal = static_cast(GPU_texture_read(input_normal, data_format, 0)); + /* If the albedo and normal inputs are not single value inputs, set the normal input to the + * albedo input of the filter, denoising it if needed. Notice that we also consider the albedo + * input because OIDN doesn't support denoising with only the normal auxiliary pass. */ + Result &input_normal = this->get_input("Normal"); + if (!input_albedo.is_single_value() && !input_normal.is_single_value()) { + float *normal = nullptr; + if (should_denoise_auxiliary_passes()) { + normal = input_normal.derived_resources() + .denoised_auxiliary_passes + .get(this->context(), + input_normal, + DenoisedAuxiliaryPassType::Normal, + this->get_quality()) + .denoised_buffer; } else { - normal = input_normal.float_texture(); - } - - if (should_denoise_auxiliary_passes()) { - oidn::FilterRef normalFilter = device.newFilter("RT"); - this->set_filter_quality(normalFilter); - normalFilter.setImage( - "normal", normal, oidn::Format::Float3, width, height, 0, pixel_stride); - normalFilter.setImage( - "output", normal, oidn::Format::Float3, width, height, 0, pixel_stride); - normalFilter.setProgressMonitorFunction(oidn_progress_monitor_function, &context()); - normalFilter.commit(); - normalFilter.execute(); + if (this->context().use_gpu()) { + normal = static_cast(GPU_texture_read(input_normal, data_format, 0)); + temporary_buffers_to_free.append(normal); + } + else { + normal = input_normal.float_texture(); + } } filter.setImage("normal", normal, oidn::Format::Float3, width, height, 0, pixel_stride); @@ -224,16 +227,8 @@ class DenoiseOperation : public NodeOperation { }); } - /* Buffers for the CPU case are owned by the inputs, while for GPU, they are temporally read - * from the GPU texture, so they need to be freed if they were read. */ - if (this->context().use_gpu()) { - MEM_freeN(input_color); - if (albedo) { - MEM_freeN(albedo); - } - if (normal) { - MEM_freeN(normal); - } + for (float *buffer : temporary_buffers_to_free) { + MEM_freeN(buffer); } #endif } @@ -268,7 +263,7 @@ class DenoiseOperation : public NodeOperation { #ifdef WITH_OPENIMAGEDENOISE # if OIDN_VERSION_MAJOR >= 2 - OIDNQuality get_quality() + oidn::Quality get_quality() { const CMPNodeDenoiseQuality node_quality = static_cast( node_storage(bnode()).quality); @@ -278,26 +273,26 @@ class DenoiseOperation : public NodeOperation { switch (scene_quality) { # if OIDN_VERSION >= 20300 case SCE_COMPOSITOR_DENOISE_FAST: - return OIDN_QUALITY_FAST; + return oidn::Quality::Fast; # endif case SCE_COMPOSITOR_DENOISE_BALANCED: - return OIDN_QUALITY_BALANCED; + return oidn::Quality::Balanced; case SCE_COMPOSITOR_DENOISE_HIGH: default: - return OIDN_QUALITY_HIGH; + return oidn::Quality::High; } } switch (node_quality) { # if OIDN_VERSION >= 20300 case CMP_NODE_DENOISE_QUALITY_FAST: - return OIDN_QUALITY_FAST; + return oidn::Quality::Fast; # endif case CMP_NODE_DENOISE_QUALITY_BALANCED: - return OIDN_QUALITY_BALANCED; + return oidn::Quality::Balanced; case CMP_NODE_DENOISE_QUALITY_HIGH: default: - return OIDN_QUALITY_HIGH; + return oidn::Quality::High; } } # endif /* OIDN_VERSION_MAJOR >= 2 */ @@ -305,7 +300,7 @@ class DenoiseOperation : public NodeOperation { void set_filter_quality([[maybe_unused]] oidn::FilterRef &filter) { # if OIDN_VERSION_MAJOR >= 2 - OIDNQuality quality = this->get_quality(); + oidn::Quality quality = this->get_quality(); filter.set("quality", quality); # endif } diff --git a/source/blender/render/CMakeLists.txt b/source/blender/render/CMakeLists.txt index c0366bcba36..e5c66c657e0 100644 --- a/source/blender/render/CMakeLists.txt +++ b/source/blender/render/CMakeLists.txt @@ -8,6 +8,7 @@ set(INC intern ../compositor ../compositor/cached_resources + ../compositor/derived_resources ../draw/intern ../gpu/intern ../makesrna