diff options
7 files changed, 454 insertions, 19 deletions
diff --git a/source/blender/compositor/COM_defines.h b/source/blender/compositor/COM_defines.h index 5a52d216117..857cbf0beee 100644 --- a/source/blender/compositor/COM_defines.h +++ b/source/blender/compositor/COM_defines.h @@ -62,6 +62,24 @@ constexpr int COM_data_type_num_channels(const DataType datatype) constexpr int COM_DATA_TYPE_VALUE_CHANNELS = COM_data_type_num_channels(DataType::Value); constexpr int COM_DATA_TYPE_COLOR_CHANNELS = COM_data_type_num_channels(DataType::Color); +constexpr float COM_VALUE_ZERO[1] = {0.0f}; + +/** + * Utility to get data type for given number of channels. + */ +constexpr DataType COM_num_channels_data_type(const int num_channels) +{ + switch (num_channels) { + case 1: + return DataType::Value; + case 3: + return DataType::Vector; + case 4: + default: + return DataType::Color; + } +} + // configurable items // chunk size determination diff --git a/source/blender/compositor/intern/COM_MemoryBuffer.cc b/source/blender/compositor/intern/COM_MemoryBuffer.cc index 8c30d3215d7..44d3f059374 100644 --- a/source/blender/compositor/intern/COM_MemoryBuffer.cc +++ b/source/blender/compositor/intern/COM_MemoryBuffer.cc @@ -18,10 +18,31 @@ #include "COM_MemoryBuffer.h" +#include "IMB_colormanagement.h" +#include "IMB_imbuf_types.h" #include "MEM_guardedalloc.h" +#define ASSERT_BUFFER_CONTAINS_AREA(buf, area) \ + BLI_assert(BLI_rcti_inside_rcti(&(buf)->get_rect(), &(area))) + +#define ASSERT_BUFFER_CONTAINS_AREA_AT_COORDS(buf, area, x, y) \ + BLI_assert((buf)->get_rect().xmin <= (x)); \ + BLI_assert((buf)->get_rect().ymin <= (y)); \ + BLI_assert((buf)->get_rect().xmax >= (x) + BLI_rcti_size_x(&(area))); \ + BLI_assert((buf)->get_rect().ymax >= (y) + BLI_rcti_size_y(&(area))) + +#define ASSERT_VALID_ELEM_SIZE(buf, channel_offset, elem_size) \ + BLI_assert((buf)->get_num_channels() <= (channel_offset) + (elem_size)) + namespace blender::compositor { +static rcti create_rect(const int width, const int height) +{ + rcti rect; + BLI_rcti_init(&rect, 0, width, 0, height); + return rect; +} + MemoryBuffer::MemoryBuffer(MemoryProxy *memoryProxy, const rcti &rect, MemoryBufferState state) { m_rect = rect; @@ -30,6 +51,7 @@ MemoryBuffer::MemoryBuffer(MemoryProxy *memoryProxy, const rcti &rect, MemoryBuf this->m_num_channels = COM_data_type_num_channels(memoryProxy->getDataType()); this->m_buffer = (float *)MEM_mallocN_aligned( sizeof(float) * buffer_len() * this->m_num_channels, 16, "COM_MemoryBuffer"); + owns_data_ = true; this->m_state = state; this->m_datatype = memoryProxy->getDataType(); @@ -44,12 +66,44 @@ MemoryBuffer::MemoryBuffer(DataType dataType, const rcti &rect, bool is_a_single this->m_num_channels = COM_data_type_num_channels(dataType); this->m_buffer = (float *)MEM_mallocN_aligned( sizeof(float) * buffer_len() * this->m_num_channels, 16, "COM_MemoryBuffer"); + owns_data_ = true; this->m_state = MemoryBufferState::Temporary; this->m_datatype = dataType; set_strides(); } +/** + * Construct MemoryBuffer from a float buffer. MemoryBuffer is not responsible for + * freeing it. + */ +MemoryBuffer::MemoryBuffer( + float *buffer, int num_channels, int width, int height, bool is_a_single_elem) + : MemoryBuffer(buffer, num_channels, create_rect(width, height), is_a_single_elem) +{ +} + +/** + * Construct MemoryBuffer from a float buffer area. MemoryBuffer is not responsible for + * freeing given buffer. + */ +MemoryBuffer::MemoryBuffer(float *buffer, + const int num_channels, + const rcti &rect, + const bool is_a_single_elem) +{ + m_rect = rect; + m_is_a_single_elem = is_a_single_elem; + m_memoryProxy = nullptr; + m_num_channels = num_channels; + m_datatype = COM_num_channels_data_type(num_channels); + m_buffer = buffer; + owns_data_ = false; + m_state = MemoryBufferState::Temporary; + + set_strides(); +} + MemoryBuffer::MemoryBuffer(const MemoryBuffer &src) : MemoryBuffer(src.m_datatype, src.m_rect, false) { @@ -112,31 +166,195 @@ float MemoryBuffer::get_max_value(const rcti &rect) const MemoryBuffer::~MemoryBuffer() { - if (this->m_buffer) { + if (this->m_buffer && owns_data_) { MEM_freeN(this->m_buffer); this->m_buffer = nullptr; } } -void MemoryBuffer::fill_from(const MemoryBuffer &src) +void MemoryBuffer::copy_from(const MemoryBuffer *src, const rcti &area) { - BLI_assert(!this->is_a_single_elem()); + copy_from(src, area, area.xmin, area.ymin); +} + +void MemoryBuffer::copy_from(const MemoryBuffer *src, + const rcti &area, + const int to_x, + const int to_y) +{ + BLI_assert(this->get_num_channels() == src->get_num_channels()); + copy_from(src, area, 0, src->get_num_channels(), to_x, to_y, 0); +} + +void MemoryBuffer::copy_from(const MemoryBuffer *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_channel_offset) +{ + copy_from(src, area, channel_offset, elem_size, area.xmin, area.ymin, to_channel_offset); +} + +void MemoryBuffer::copy_from(const MemoryBuffer *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_x, + const int to_y, + const int to_channel_offset) +{ + if (this->is_a_single_elem()) { + copy_single_elem_from(src, channel_offset, elem_size, to_channel_offset); + } + else if (!src->is_a_single_elem() && elem_size == src->get_num_channels() && + elem_size == this->get_num_channels()) { + BLI_assert(to_channel_offset == 0); + BLI_assert(channel_offset == 0); + copy_rows_from(src, area, to_x, to_y); + } + else { + copy_elems_from(src, area, channel_offset, elem_size, to_x, to_y, to_channel_offset); + } +} + +void MemoryBuffer::copy_from(const uchar *src, const rcti &area) +{ + copy_from(src, area, 0, this->get_num_channels(), this->get_num_channels(), 0); +} + +void MemoryBuffer::copy_from(const uchar *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int elem_stride, + const int to_channel_offset) +{ + copy_from( + src, area, channel_offset, elem_size, elem_stride, area.xmin, area.ymin, to_channel_offset); +} + +void MemoryBuffer::copy_from(const uchar *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int elem_stride, + const int to_x, + const int to_y, + const int to_channel_offset) +{ + ASSERT_BUFFER_CONTAINS_AREA_AT_COORDS(this, area, to_x, to_y); + ASSERT_VALID_ELEM_SIZE(this, to_channel_offset, elem_size); + + const int width = BLI_rcti_size_x(&area); + const int height = BLI_rcti_size_y(&area); + const int src_row_stride = width * elem_stride; + const uchar *const src_start = src + area.ymin * src_row_stride + channel_offset; + for (int y = 0; y < height; y++) { + const uchar *from_elem = src_start + y * src_row_stride; + float *to_elem = &this->get_value(to_x, to_y + y, to_channel_offset); + const float *row_end = to_elem + width * this->elem_stride; + while (to_elem < row_end) { + for (int i = 0; i < elem_size; i++) { + to_elem[i] = ((float)from_elem[i]) * (1.0f / 255.0f); + } + to_elem += this->elem_stride; + from_elem += elem_stride; + } + } +} + +static void colorspace_to_scene_linear(MemoryBuffer *buf, const rcti &area, ColorSpace *colorspace) +{ + const int width = BLI_rcti_size_x(&area); + const int height = BLI_rcti_size_y(&area); + float *out = buf->get_elem(area.xmin, area.ymin); + /* If area allows continuous memory do conversion in one step. Otherwise per row. */ + if (buf->getWidth() == width) { + IMB_colormanagement_colorspace_to_scene_linear( + out, width, height, buf->get_num_channels(), colorspace, false); + } + else { + for (int y = 0; y < height; y++) { + IMB_colormanagement_colorspace_to_scene_linear( + out, width, 1, buf->get_num_channels(), colorspace, false); + out += buf->row_stride; + } + } +} + +void MemoryBuffer::copy_from(const ImBuf *src, const rcti &area, const bool ensure_linear_space) +{ + copy_from(src, area, 0, this->get_num_channels(), 0, ensure_linear_space); +} - unsigned int otherY; - unsigned int minX = MAX2(this->m_rect.xmin, src.m_rect.xmin); - unsigned int maxX = MIN2(this->m_rect.xmax, src.m_rect.xmax); - unsigned int minY = MAX2(this->m_rect.ymin, src.m_rect.ymin); - unsigned int maxY = MIN2(this->m_rect.ymax, src.m_rect.ymax); - int offset; - int otherOffset; +void MemoryBuffer::copy_from(const ImBuf *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_channel_offset, + const bool ensure_linear_space) +{ + copy_from(src, + area, + channel_offset, + elem_size, + area.xmin, + area.ymin, + to_channel_offset, + ensure_linear_space); +} - for (otherY = minY; otherY < maxY; otherY++) { - otherOffset = src.get_coords_offset(minX, otherY); - offset = this->get_coords_offset(minX, otherY); - memcpy(&this->m_buffer[offset], - &src.m_buffer[otherOffset], - (maxX - minX) * this->m_num_channels * sizeof(float)); +void MemoryBuffer::copy_from(const ImBuf *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_x, + const int to_y, + const int to_channel_offset, + const bool ensure_linear_space) +{ + if (src->rect_float) { + const MemoryBuffer mem_buf(src->rect_float, src->channels, src->x, src->y, false); + copy_from(&mem_buf, area, channel_offset, elem_size, to_x, to_y, to_channel_offset); } + else if (src->rect) { + const uchar *uc_buf = (uchar *)src->rect; + const int elem_stride = src->channels; + copy_from(uc_buf, area, channel_offset, elem_size, elem_stride, to_x, to_y, to_channel_offset); + if (ensure_linear_space) { + colorspace_to_scene_linear(this, area, src->rect_colorspace); + } + } + else { + /* Empty ImBuf source. Fill destination with empty values. */ + const float *zero_elem = new float[elem_size]{0}; + fill(area, to_channel_offset, zero_elem, elem_size); + delete[] zero_elem; + } +} + +void MemoryBuffer::fill(const rcti &area, const float *value) +{ + fill(area, 0, value, this->get_num_channels()); +} + +void MemoryBuffer::fill(const rcti &area, + const int channel_offset, + const float *value, + const int value_size) +{ + const MemoryBuffer single_elem(const_cast<float *>(value), value_size, this->get_rect(), true); + copy_from(&single_elem, area, 0, value_size, area.xmin, area.ymin, channel_offset); +} + +void MemoryBuffer::fill_from(const MemoryBuffer &src) +{ + rcti overlap; + overlap.xmin = MAX2(this->m_rect.xmin, src.m_rect.xmin); + overlap.xmax = MIN2(this->m_rect.xmax, src.m_rect.xmax); + overlap.ymin = MAX2(this->m_rect.ymin, src.m_rect.ymin); + overlap.ymax = MIN2(this->m_rect.ymax, src.m_rect.ymax); + copy_from(&src, overlap); } void MemoryBuffer::writePixel(int x, int y, const float color[4]) @@ -196,4 +414,70 @@ void MemoryBuffer::readEWA(float *result, const float uv[2], const float derivat } } +void MemoryBuffer::copy_single_elem_from(const MemoryBuffer *src, + const int channel_offset, + const int elem_size, + const int to_channel_offset) +{ + ASSERT_VALID_ELEM_SIZE(this, to_channel_offset, elem_size); + ASSERT_VALID_ELEM_SIZE(src, channel_offset, elem_size); + BLI_assert(this->is_a_single_elem()); + + float *to_elem = &this->get_value( + this->get_rect().xmin, this->get_rect().ymin, to_channel_offset); + const float *from_elem = &src->get_value( + src->get_rect().xmin, src->get_rect().ymin, channel_offset); + const int elem_bytes = elem_size * sizeof(float); + memcpy(to_elem, from_elem, elem_bytes); +} + +void MemoryBuffer::copy_rows_from(const MemoryBuffer *src, + const rcti &area, + const int to_x, + const int to_y) +{ + ASSERT_BUFFER_CONTAINS_AREA(src, area); + ASSERT_BUFFER_CONTAINS_AREA_AT_COORDS(this, area, to_x, to_y); + BLI_assert(this->get_num_channels() == src->get_num_channels()); + BLI_assert(!this->is_a_single_elem()); + BLI_assert(!src->is_a_single_elem()); + + const int width = BLI_rcti_size_x(&area); + const int height = BLI_rcti_size_y(&area); + const int row_bytes = this->get_num_channels() * width * sizeof(float); + for (int y = 0; y < height; y++) { + float *to_row = this->get_elem(to_x, to_y + y); + const float *from_row = src->get_elem(area.xmin, area.ymin + y); + memcpy(to_row, from_row, row_bytes); + } +} + +void MemoryBuffer::copy_elems_from(const MemoryBuffer *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_x, + const int to_y, + const int to_channel_offset) +{ + ASSERT_BUFFER_CONTAINS_AREA(src, area); + ASSERT_BUFFER_CONTAINS_AREA_AT_COORDS(this, area, to_x, to_y); + ASSERT_VALID_ELEM_SIZE(this, to_channel_offset, elem_size); + ASSERT_VALID_ELEM_SIZE(src, channel_offset, elem_size); + + const int width = BLI_rcti_size_x(&area); + const int height = BLI_rcti_size_y(&area); + const int elem_bytes = elem_size * sizeof(float); + for (int y = 0; y < height; y++) { + float *to_elem = &this->get_value(to_x, to_y + y, to_channel_offset); + const float *from_elem = &src->get_value(area.xmin, area.ymin + y, channel_offset); + const float *row_end = to_elem + width * this->elem_stride; + while (to_elem < row_end) { + memcpy(to_elem, from_elem, elem_bytes); + to_elem += this->elem_stride; + from_elem += src->elem_stride; + } + } +} + } // namespace blender::compositor diff --git a/source/blender/compositor/intern/COM_MemoryBuffer.h b/source/blender/compositor/intern/COM_MemoryBuffer.h index 045dc996e3e..575bb2fe383 100644 --- a/source/blender/compositor/intern/COM_MemoryBuffer.h +++ b/source/blender/compositor/intern/COM_MemoryBuffer.h @@ -106,6 +106,11 @@ class MemoryBuffer { */ bool m_is_a_single_elem; + /** + * Whether MemoryBuffer owns buffer data. + */ + bool owns_data_; + public: /** * \brief construct new temporarily MemoryBuffer for an area @@ -117,6 +122,11 @@ class MemoryBuffer { */ MemoryBuffer(DataType datatype, const rcti &rect, bool is_a_single_elem = false); + MemoryBuffer( + float *buffer, int num_channels, int width, int height, bool is_a_single_elem = false); + + MemoryBuffer(float *buffer, int num_channels, const rcti &rect, bool is_a_single_elem = false); + /** * Copy constructor */ @@ -223,7 +233,7 @@ class MemoryBuffer { return is_a_single_elem() ? 1 : getHeight(); } - uint8_t get_num_channels() + uint8_t get_num_channels() const { return this->m_num_channels; } @@ -404,6 +414,53 @@ class MemoryBuffer { return this->m_state == MemoryBufferState::Temporary; } + void copy_from(const MemoryBuffer *src, const rcti &area); + void copy_from(const MemoryBuffer *src, const rcti &area, int to_x, int to_y); + void copy_from(const MemoryBuffer *src, + const rcti &area, + int channel_offset, + int elem_size, + int to_channel_offset); + void copy_from(const MemoryBuffer *src, + const rcti &area, + int channel_offset, + int elem_size, + int to_x, + int to_y, + int to_channel_offset); + void copy_from(const uchar *src, const rcti &area); + void copy_from(const uchar *src, + const rcti &area, + int channel_offset, + int elem_size, + int elem_stride, + int to_channel_offset); + void copy_from(const uchar *src, + const rcti &area, + int channel_offset, + int elem_size, + int elem_stride, + int to_x, + int to_y, + int to_channel_offset); + void copy_from(const struct ImBuf *src, const rcti &area, bool ensure_linear_space = false); + void copy_from(const struct ImBuf *src, + const rcti &area, + int channel_offset, + int elem_size, + int to_channel_offset, + bool ensure_linear_space = false); + void copy_from(const struct ImBuf *src, + const rcti &src_area, + int channel_offset, + int elem_size, + int to_x, + int to_y, + int to_channel_offset, + bool ensure_linear_space = false); + + void fill(const rcti &area, const float *value); + void fill(const rcti &area, int channel_offset, const float *value, int value_size); /** * \brief add the content from otherBuffer to this MemoryBuffer * \param otherBuffer: source buffer @@ -452,6 +509,22 @@ class MemoryBuffer { return get_memory_width() * get_memory_height(); } + void copy_single_elem_from(const MemoryBuffer *src, + int channel_offset, + int elem_size, + const int to_channel_offset); + void copy_rows_from(const MemoryBuffer *src, + const rcti &src_area, + const int to_x, + const int to_y); + void copy_elems_from(const MemoryBuffer *src, + const rcti &area, + const int channel_offset, + const int elem_size, + const int to_x, + const int to_y, + const int to_channel_offset); + #ifdef WITH_CXX_GUARDEDALLOC MEM_CXX_CLASS_ALLOC_FUNCS("COM:MemoryBuffer") #endif diff --git a/source/blender/compositor/operations/COM_ImageOperation.cc b/source/blender/compositor/operations/COM_ImageOperation.cc index a1d401d4499..e78d389410f 100644 --- a/source/blender/compositor/operations/COM_ImageOperation.cc +++ b/source/blender/compositor/operations/COM_ImageOperation.cc @@ -44,6 +44,7 @@ BaseImageOperation::BaseImageOperation() this->m_imageheight = 0; this->m_framenumber = 0; this->m_depthBuffer = nullptr; + depth_buffer_ = nullptr; this->m_numberOfChannels = 0; this->m_rd = nullptr; this->m_viewName = nullptr; @@ -91,6 +92,9 @@ void BaseImageOperation::initExecution() this->m_imageFloatBuffer = stackbuf->rect_float; this->m_imageByteBuffer = stackbuf->rect; this->m_depthBuffer = stackbuf->zbuf_float; + if (stackbuf->zbuf_float) { + depth_buffer_ = new MemoryBuffer(stackbuf->zbuf_float, 1, stackbuf->x, stackbuf->y); + } this->m_imagewidth = stackbuf->x; this->m_imageheight = stackbuf->y; this->m_numberOfChannels = stackbuf->channels; @@ -102,6 +106,10 @@ void BaseImageOperation::deinitExecution() this->m_imageFloatBuffer = nullptr; this->m_imageByteBuffer = nullptr; BKE_image_release_ibuf(this->m_image, this->m_buffer, nullptr); + if (depth_buffer_) { + delete depth_buffer_; + depth_buffer_ = nullptr; + } } void BaseImageOperation::determineResolution(unsigned int resolution[2], @@ -170,6 +178,13 @@ void ImageOperation::executePixelSampled(float output[4], float x, float y, Pixe } } +void ImageOperation::update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> UNUSED(inputs)) +{ + output->copy_from(m_buffer, area, true); +} + void ImageAlphaOperation::executePixelSampled(float output[4], float x, float y, @@ -187,6 +202,13 @@ void ImageAlphaOperation::executePixelSampled(float output[4], } } +void ImageAlphaOperation::update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> UNUSED(inputs)) +{ + output->copy_from(m_buffer, area, 3, COM_DATA_TYPE_VALUE_CHANNELS, 0); +} + void ImageDepthOperation::executePixelSampled(float output[4], float x, float y, @@ -206,4 +228,16 @@ void ImageDepthOperation::executePixelSampled(float output[4], } } +void ImageDepthOperation::update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> UNUSED(inputs)) +{ + if (depth_buffer_) { + output->copy_from(depth_buffer_, area); + } + else { + output->fill(area, COM_VALUE_ZERO); + } +} + } // namespace blender::compositor diff --git a/source/blender/compositor/operations/COM_ImageOperation.h b/source/blender/compositor/operations/COM_ImageOperation.h index 58373663db5..f8b4239c9f8 100644 --- a/source/blender/compositor/operations/COM_ImageOperation.h +++ b/source/blender/compositor/operations/COM_ImageOperation.h @@ -21,7 +21,7 @@ #include "BKE_image.h" #include "BLI_listbase.h" #include "BLI_utildefines.h" -#include "COM_NodeOperation.h" +#include "COM_MultiThreadedOperation.h" #include "MEM_guardedalloc.h" #include "RE_pipeline.h" @@ -32,14 +32,17 @@ namespace blender::compositor { /** * \brief Base class for all image operations */ -class BaseImageOperation : public NodeOperation { +class BaseImageOperation : public MultiThreadedOperation { protected: ImBuf *m_buffer; Image *m_image; ImageUser *m_imageUser; + /* TODO: Remove raw buffers when removing Tiled implementation. */ float *m_imageFloatBuffer; unsigned int *m_imageByteBuffer; float *m_depthBuffer; + + MemoryBuffer *depth_buffer_; int m_imageheight; int m_imagewidth; int m_framenumber; @@ -87,6 +90,10 @@ class ImageOperation : public BaseImageOperation { */ ImageOperation(); void executePixelSampled(float output[4], float x, float y, PixelSampler sampler) override; + + void update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> inputs) override; }; class ImageAlphaOperation : public BaseImageOperation { public: @@ -95,6 +102,10 @@ class ImageAlphaOperation : public BaseImageOperation { */ ImageAlphaOperation(); void executePixelSampled(float output[4], float x, float y, PixelSampler sampler) override; + + void update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> inputs) override; }; class ImageDepthOperation : public BaseImageOperation { public: @@ -103,6 +114,10 @@ class ImageDepthOperation : public BaseImageOperation { */ ImageDepthOperation(); void executePixelSampled(float output[4], float x, float y, PixelSampler sampler) override; + + void update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> inputs) override; }; } // namespace blender::compositor diff --git a/source/blender/compositor/operations/COM_MultilayerImageOperation.cc b/source/blender/compositor/operations/COM_MultilayerImageOperation.cc index 647e93225e5..3a5de944a00 100644 --- a/source/blender/compositor/operations/COM_MultilayerImageOperation.cc +++ b/source/blender/compositor/operations/COM_MultilayerImageOperation.cc @@ -51,6 +51,13 @@ ImBuf *MultilayerBaseOperation::getImBuf() return nullptr; } +void MultilayerBaseOperation::update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> UNUSED(inputs)) +{ + output->copy_from(m_buffer, area); +} + std::unique_ptr<MetaData> MultilayerColorOperation::getMetaData() { BLI_assert(this->m_buffer); diff --git a/source/blender/compositor/operations/COM_MultilayerImageOperation.h b/source/blender/compositor/operations/COM_MultilayerImageOperation.h index 6e6062cf854..a682ca1941c 100644 --- a/source/blender/compositor/operations/COM_MultilayerImageOperation.h +++ b/source/blender/compositor/operations/COM_MultilayerImageOperation.h @@ -37,6 +37,10 @@ class MultilayerBaseOperation : public BaseImageOperation { * Constructor */ MultilayerBaseOperation(RenderLayer *render_layer, RenderPass *render_pass, int view); + + void update_memory_buffer_partial(MemoryBuffer *output, + const rcti &area, + Span<MemoryBuffer *> inputs) override; }; class MultilayerColorOperation : public MultilayerBaseOperation { |