This patches refactors the compositor File Output mechanism and implements the file output node for the Realtime Compositor. The refactor was done for the following reasons: 1. The existing file output mechanism relied on a global EXR image resource where the result of each compositor execution for each view was accumulated and stored in the global resource, until the last view is executed, when the EXR is finally saved. Aside from relying on global resources, this can cause effective memory leaks since the compositor can be interrupted before the EXR is written and closed. 2. We need common code to share between all compositors since we now have multiple compositor implementations. 3. We needed to take the opportunity to fix some of the issues with the existing implementation, like lossy compression of data passes, and inability to save single values passes. The refactor first introduced a new structure called the Compositor Render Context. This context stores compositor information related to the render pipeline and is persistent across all compositor executions of all views. Its extended lifetime relative to a single compositor execution lends itself well to store data that is accumulated across views. The context currently has a map of File Output objects. Those objects wrap a Render Result structure and can be used to construct multi-view images which can then be saved after all views are executed using the existing BKE_image_render_write function. Minor adjustments were made to the BKE and RE modules to allow saving using the BKE_image_render_write function. Namely, the function now allows the use of a source image format for saving as well as the ability to not save the render result as a render by introducing two new default arguments. Further, for multi-layer EXR saving, the existent of a single unnamed render layer will omit the layer name from the EXR channel full name, and only the pass, view, and channel ID will remain. Finally, the Render Result to Image Buffer conversion now take he number of channels into account, instead of always assuming color channels. The patch implements the File Output node in the Realtime Compositor using the aforementioned mechanisms, replaces the implementation of the CPU compositor using the same Realtime Compositor implementation, and setup the necessary logic in the render pipeline code. Pull Request: https://projects.blender.org/blender/blender/pulls/113982
560 lines
16 KiB
C++
560 lines
16 KiB
C++
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later */
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
#include "BLI_threads.h"
|
|
#include "BLI_vector.hh"
|
|
|
|
#include "MEM_guardedalloc.h"
|
|
|
|
#include "DNA_ID.h"
|
|
|
|
#include "BKE_global.h"
|
|
#include "BKE_image.h"
|
|
#include "BKE_node.hh"
|
|
#include "BKE_scene.h"
|
|
|
|
#include "DRW_engine.h"
|
|
#include "DRW_render.h"
|
|
|
|
#include "IMB_colormanagement.h"
|
|
#include "IMB_imbuf.h"
|
|
|
|
#include "DEG_depsgraph_query.hh"
|
|
|
|
#include "COM_context.hh"
|
|
#include "COM_evaluator.hh"
|
|
#include "COM_render_context.hh"
|
|
|
|
#include "RE_compositor.hh"
|
|
#include "RE_pipeline.h"
|
|
|
|
#include "render_types.h"
|
|
|
|
namespace blender::render {
|
|
|
|
/**
|
|
* Render Texture Pool
|
|
*
|
|
* TODO: should share pool with draw manager. It needs some globals initialization figured out
|
|
* there first.
|
|
*/
|
|
class TexturePool : public realtime_compositor::TexturePool {
|
|
private:
|
|
/** Textures that are not yet used and are available to be acquired. After evaluation, any
|
|
* texture in this map should be freed because it was not acquired in the evaluation and is thus
|
|
* unused. Textures removed from this map should be moved to the textures_in_use_ map when
|
|
* acquired. */
|
|
Map<realtime_compositor::TexturePoolKey, Vector<GPUTexture *>> available_textures_;
|
|
/** Textures that were acquired in this compositor evaluation. After evaluation, those textures
|
|
* are moved to the available_textures_ map to be acquired in the next evaluation. */
|
|
Map<realtime_compositor::TexturePoolKey, Vector<GPUTexture *>> textures_in_use_;
|
|
|
|
public:
|
|
virtual ~TexturePool()
|
|
{
|
|
for (Vector<GPUTexture *> &available_textures : available_textures_.values()) {
|
|
for (GPUTexture *texture : available_textures) {
|
|
GPU_texture_free(texture);
|
|
}
|
|
}
|
|
|
|
for (Vector<GPUTexture *> &textures_in_use : textures_in_use_.values()) {
|
|
for (GPUTexture *texture : textures_in_use) {
|
|
GPU_texture_free(texture);
|
|
}
|
|
}
|
|
}
|
|
|
|
GPUTexture *allocate_texture(int2 size, eGPUTextureFormat format) override
|
|
{
|
|
const realtime_compositor::TexturePoolKey key(size, format);
|
|
Vector<GPUTexture *> &available_textures = available_textures_.lookup_or_add_default(key);
|
|
GPUTexture *texture = nullptr;
|
|
if (available_textures.is_empty()) {
|
|
texture = GPU_texture_create_2d("compositor_texture_pool",
|
|
size.x,
|
|
size.y,
|
|
1,
|
|
format,
|
|
GPU_TEXTURE_USAGE_GENERAL,
|
|
nullptr);
|
|
}
|
|
else {
|
|
texture = available_textures.pop_last();
|
|
}
|
|
|
|
textures_in_use_.lookup_or_add_default(key).append(texture);
|
|
return texture;
|
|
}
|
|
|
|
/** Should be called after compositor evaluation to free unused textures and reset the texture
|
|
* pool. */
|
|
void free_unused_and_reset()
|
|
{
|
|
/* Free all textures in the available textures vectors. The fact that they still exist in those
|
|
* vectors after evaluation means they were not acquired during the evaluation, and are thus
|
|
* consequently no longer used. */
|
|
for (Vector<GPUTexture *> &available_textures : available_textures_.values()) {
|
|
for (GPUTexture *texture : available_textures) {
|
|
GPU_texture_free(texture);
|
|
}
|
|
}
|
|
|
|
/* Move all textures in-use to be available textures for the next evaluation. */
|
|
available_textures_ = textures_in_use_;
|
|
textures_in_use_.clear();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Render Context Data
|
|
*
|
|
* Stored separately from the context so we can update it without losing any cached
|
|
* data from the context.
|
|
*/
|
|
class ContextInputData {
|
|
public:
|
|
const Scene *scene;
|
|
const RenderData *render_data;
|
|
const bNodeTree *node_tree;
|
|
bool use_file_output;
|
|
std::string view_name;
|
|
realtime_compositor::RenderContext *render_context;
|
|
|
|
ContextInputData(const Scene &scene,
|
|
const RenderData &render_data,
|
|
const bNodeTree &node_tree,
|
|
const bool use_file_output,
|
|
const char *view_name,
|
|
realtime_compositor::RenderContext *render_context)
|
|
: scene(&scene),
|
|
render_data(&render_data),
|
|
node_tree(&node_tree),
|
|
use_file_output(use_file_output),
|
|
view_name(view_name),
|
|
render_context(render_context)
|
|
{
|
|
}
|
|
};
|
|
|
|
/* Render Context Data */
|
|
|
|
class Context : public realtime_compositor::Context {
|
|
private:
|
|
/* Input data. */
|
|
ContextInputData input_data_;
|
|
|
|
/* Output combined texture. */
|
|
GPUTexture *output_texture_ = nullptr;
|
|
|
|
/* Viewer output texture. */
|
|
GPUTexture *viewer_output_texture_ = nullptr;
|
|
|
|
/* Cached textures that the compositor took ownership of. */
|
|
Vector<GPUTexture *> textures_;
|
|
|
|
public:
|
|
Context(const ContextInputData &input_data, TexturePool &texture_pool)
|
|
: realtime_compositor::Context(texture_pool), input_data_(input_data)
|
|
{
|
|
}
|
|
|
|
virtual ~Context()
|
|
{
|
|
GPU_TEXTURE_FREE_SAFE(output_texture_);
|
|
GPU_TEXTURE_FREE_SAFE(viewer_output_texture_);
|
|
for (GPUTexture *texture : textures_) {
|
|
GPU_texture_free(texture);
|
|
}
|
|
}
|
|
|
|
void update_input_data(const ContextInputData &input_data)
|
|
{
|
|
input_data_ = input_data;
|
|
}
|
|
|
|
const Scene &get_scene() const override
|
|
{
|
|
return *input_data_.scene;
|
|
}
|
|
|
|
const bNodeTree &get_node_tree() const override
|
|
{
|
|
return *input_data_.node_tree;
|
|
}
|
|
|
|
bool use_file_output() const override
|
|
{
|
|
return input_data_.use_file_output;
|
|
}
|
|
|
|
bool use_composite_output() const override
|
|
{
|
|
return true;
|
|
}
|
|
|
|
const RenderData &get_render_data() const override
|
|
{
|
|
return *(input_data_.render_data);
|
|
}
|
|
|
|
int2 get_render_size() const override
|
|
{
|
|
int width, height;
|
|
BKE_render_resolution(input_data_.render_data, false, &width, &height);
|
|
return int2(width, height);
|
|
}
|
|
|
|
rcti get_compositing_region() const override
|
|
{
|
|
const int2 render_size = get_render_size();
|
|
const rcti render_region = rcti{0, render_size.x, 0, render_size.y};
|
|
|
|
return render_region;
|
|
}
|
|
|
|
GPUTexture *get_output_texture() override
|
|
{
|
|
/* TODO: just a temporary hack, needs to get stored in RenderResult,
|
|
* once that supports GPU buffers. */
|
|
if (output_texture_ == nullptr) {
|
|
const int2 size = get_render_size();
|
|
output_texture_ = GPU_texture_create_2d(
|
|
"compositor_output_texture",
|
|
size.x,
|
|
size.y,
|
|
1,
|
|
get_precision() == realtime_compositor::ResultPrecision::Half ? GPU_RGBA16F :
|
|
GPU_RGBA32F,
|
|
GPU_TEXTURE_USAGE_GENERAL,
|
|
nullptr);
|
|
}
|
|
|
|
return output_texture_;
|
|
}
|
|
|
|
GPUTexture *get_viewer_output_texture(int2 size) override
|
|
{
|
|
/* Re-create texture if the viewer size changes. */
|
|
if (viewer_output_texture_) {
|
|
const int current_width = GPU_texture_width(viewer_output_texture_);
|
|
const int current_height = GPU_texture_height(viewer_output_texture_);
|
|
|
|
if (current_width != size.x || current_height != size.y) {
|
|
GPU_TEXTURE_FREE_SAFE(viewer_output_texture_);
|
|
viewer_output_texture_ = nullptr;
|
|
}
|
|
}
|
|
|
|
/* TODO: just a temporary hack, needs to get stored in RenderResult,
|
|
* once that supports GPU buffers. */
|
|
if (viewer_output_texture_ == nullptr) {
|
|
viewer_output_texture_ = GPU_texture_create_2d(
|
|
"compositor_viewer_output_texture",
|
|
size.x,
|
|
size.y,
|
|
1,
|
|
get_precision() == realtime_compositor::ResultPrecision::Half ? GPU_RGBA16F :
|
|
GPU_RGBA32F,
|
|
GPU_TEXTURE_USAGE_GENERAL,
|
|
nullptr);
|
|
}
|
|
|
|
return viewer_output_texture_;
|
|
}
|
|
|
|
GPUTexture *get_input_texture(const Scene *scene,
|
|
int view_layer_id,
|
|
const char *pass_name) override
|
|
{
|
|
Render *re = RE_GetSceneRender(scene);
|
|
RenderResult *rr = nullptr;
|
|
GPUTexture *input_texture = nullptr;
|
|
|
|
if (re) {
|
|
rr = RE_AcquireResultRead(re);
|
|
}
|
|
|
|
if (rr) {
|
|
ViewLayer *view_layer = (ViewLayer *)BLI_findlink(&scene->view_layers, view_layer_id);
|
|
if (view_layer) {
|
|
RenderLayer *rl = RE_GetRenderLayer(rr, view_layer->name);
|
|
if (rl) {
|
|
RenderPass *rpass = RE_pass_find_by_name(rl, pass_name, get_view_name().data());
|
|
|
|
if (rpass && rpass->ibuf && rpass->ibuf->float_buffer.data) {
|
|
input_texture = RE_pass_ensure_gpu_texture_cache(re, rpass);
|
|
|
|
if (input_texture) {
|
|
/* Don't assume render keeps texture around, add our own reference. */
|
|
GPU_texture_ref(input_texture);
|
|
textures_.append(input_texture);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (re) {
|
|
RE_ReleaseResult(re);
|
|
re = nullptr;
|
|
}
|
|
|
|
return input_texture;
|
|
}
|
|
|
|
StringRef get_view_name() override
|
|
{
|
|
return input_data_.view_name;
|
|
}
|
|
|
|
realtime_compositor::ResultPrecision get_precision() const override
|
|
{
|
|
switch (input_data_.node_tree->precision) {
|
|
case NODE_TREE_COMPOSITOR_PRECISION_AUTO:
|
|
/* Auto uses full precision for final renders and half procession otherwise. File outputs
|
|
* are only used in final renders, so use that as a condition. */
|
|
if (use_file_output()) {
|
|
return realtime_compositor::ResultPrecision::Full;
|
|
}
|
|
else {
|
|
return realtime_compositor::ResultPrecision::Half;
|
|
}
|
|
case NODE_TREE_COMPOSITOR_PRECISION_FULL:
|
|
return realtime_compositor::ResultPrecision::Full;
|
|
}
|
|
|
|
BLI_assert_unreachable();
|
|
return realtime_compositor::ResultPrecision::Full;
|
|
}
|
|
|
|
void set_info_message(StringRef /*message*/) const override
|
|
{
|
|
/* TODO: ignored for now. Currently only used to communicate incomplete node support
|
|
* which is already shown on the node itself.
|
|
*
|
|
* Perhaps this overall info message could be replaced by a boolean indicating
|
|
* incomplete support, and leave more specific message to individual nodes? */
|
|
}
|
|
|
|
IDRecalcFlag query_id_recalc_flag(ID *id) const override
|
|
{
|
|
DrawEngineType *owner = (DrawEngineType *)this;
|
|
DrawData *draw_data = DRW_drawdata_ensure(id, owner, sizeof(DrawData), nullptr, nullptr);
|
|
IDRecalcFlag recalc_flag = IDRecalcFlag(draw_data->recalc);
|
|
draw_data->recalc = IDRecalcFlag(0);
|
|
return recalc_flag;
|
|
}
|
|
|
|
void output_to_render_result()
|
|
{
|
|
if (!output_texture_) {
|
|
return;
|
|
}
|
|
|
|
Render *re = RE_GetSceneRender(input_data_.scene);
|
|
RenderResult *rr = RE_AcquireResultWrite(re);
|
|
|
|
if (rr) {
|
|
RenderView *rv = RE_RenderViewGetByName(rr, input_data_.view_name.c_str());
|
|
|
|
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
|
|
float *output_buffer = (float *)GPU_texture_read(output_texture_, GPU_DATA_FLOAT, 0);
|
|
|
|
if (output_buffer) {
|
|
ImBuf *ibuf = RE_RenderViewEnsureImBuf(rr, rv);
|
|
IMB_assign_float_buffer(ibuf, output_buffer, IB_TAKE_OWNERSHIP);
|
|
}
|
|
|
|
/* TODO: z-buffer output. */
|
|
|
|
rr->have_combined = true;
|
|
}
|
|
|
|
if (re) {
|
|
RE_ReleaseResult(re);
|
|
re = nullptr;
|
|
}
|
|
|
|
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_R_RESULT, "Render Result");
|
|
BKE_image_partial_update_mark_full_update(image);
|
|
BLI_thread_lock(LOCK_DRAW_IMAGE);
|
|
BKE_image_signal(G.main, image, nullptr, IMA_SIGNAL_FREE);
|
|
BLI_thread_unlock(LOCK_DRAW_IMAGE);
|
|
}
|
|
|
|
void viewer_output_to_viewer_image()
|
|
{
|
|
if (!viewer_output_texture_) {
|
|
return;
|
|
}
|
|
|
|
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_COMPOSITE, "Viewer Node");
|
|
|
|
ImageUser image_user = {nullptr};
|
|
image_user.multi_index = BKE_scene_multiview_view_id_get(input_data_.render_data,
|
|
input_data_.view_name.c_str());
|
|
|
|
if (BKE_scene_multiview_is_render_view_first(input_data_.render_data,
|
|
input_data_.view_name.c_str()))
|
|
{
|
|
BKE_image_ensure_viewer_views(input_data_.render_data, image, &image_user);
|
|
}
|
|
|
|
BLI_thread_lock(LOCK_DRAW_IMAGE);
|
|
|
|
void *lock;
|
|
ImBuf *image_buffer = BKE_image_acquire_ibuf(image, &image_user, &lock);
|
|
|
|
const int2 size = int2(GPU_texture_width(viewer_output_texture_),
|
|
GPU_texture_height(viewer_output_texture_));
|
|
if (image_buffer->x != size.x || image_buffer->y != size.y) {
|
|
imb_freerectImBuf(image_buffer);
|
|
imb_freerectfloatImBuf(image_buffer);
|
|
image_buffer->x = size.x;
|
|
image_buffer->y = size.y;
|
|
imb_addrectfloatImBuf(image_buffer, 4);
|
|
image_buffer->userflags |= IB_DISPLAY_BUFFER_INVALID;
|
|
}
|
|
|
|
BKE_image_release_ibuf(image, image_buffer, lock);
|
|
BLI_thread_unlock(LOCK_DRAW_IMAGE);
|
|
|
|
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
|
|
float *output_buffer = (float *)GPU_texture_read(viewer_output_texture_, GPU_DATA_FLOAT, 0);
|
|
|
|
std::memcpy(
|
|
image_buffer->float_buffer.data, output_buffer, size.x * size.y * 4 * sizeof(float));
|
|
|
|
MEM_freeN(output_buffer);
|
|
|
|
BKE_image_partial_update_mark_full_update(image);
|
|
if (input_data_.node_tree->runtime->update_draw) {
|
|
input_data_.node_tree->runtime->update_draw(input_data_.node_tree->runtime->udh);
|
|
}
|
|
}
|
|
|
|
realtime_compositor::RenderContext *render_context() const override
|
|
{
|
|
return input_data_.render_context;
|
|
}
|
|
};
|
|
|
|
/* Render Realtime Compositor */
|
|
|
|
class RealtimeCompositor {
|
|
private:
|
|
/* Render instance for GPU context to run compositor in. */
|
|
Render &render_;
|
|
|
|
std::unique_ptr<TexturePool> texture_pool_;
|
|
std::unique_ptr<Context> context_;
|
|
|
|
public:
|
|
RealtimeCompositor(Render &render, const ContextInputData &input_data) : render_(render)
|
|
{
|
|
BLI_assert(!BLI_thread_is_main());
|
|
|
|
/* Create resources with GPU context enabled. */
|
|
DRW_render_context_enable(&render_);
|
|
texture_pool_ = std::make_unique<TexturePool>();
|
|
context_ = std::make_unique<Context>(input_data, *texture_pool_);
|
|
DRW_render_context_disable(&render_);
|
|
}
|
|
|
|
~RealtimeCompositor()
|
|
{
|
|
/* Free resources with GPU context enabled. Cleanup may happen from the
|
|
* main thread, and we must use the main context there. */
|
|
if (BLI_thread_is_main()) {
|
|
DRW_gpu_context_enable();
|
|
}
|
|
else {
|
|
DRW_render_context_enable(&render_);
|
|
}
|
|
|
|
context_.reset();
|
|
texture_pool_.reset();
|
|
|
|
if (BLI_thread_is_main()) {
|
|
DRW_gpu_context_disable();
|
|
}
|
|
else {
|
|
DRW_render_context_disable(&render_);
|
|
}
|
|
}
|
|
|
|
/* Evaluate the compositor and output to the scene render result. */
|
|
void execute(const ContextInputData &input_data)
|
|
{
|
|
BLI_assert(!BLI_thread_is_main());
|
|
|
|
DRW_render_context_enable(&render_);
|
|
context_->update_input_data(input_data);
|
|
|
|
/* Always recreate the evaluator, as this only runs on compositing node changes and
|
|
* there is no reason to cache this. Unlike the viewport where it helps for navigation. */
|
|
{
|
|
realtime_compositor::Evaluator evaluator(*context_);
|
|
evaluator.evaluate();
|
|
}
|
|
|
|
context_->output_to_render_result();
|
|
context_->viewer_output_to_viewer_image();
|
|
texture_pool_->free_unused_and_reset();
|
|
DRW_render_context_disable(&render_);
|
|
}
|
|
};
|
|
|
|
} // namespace blender::render
|
|
|
|
void Render::compositor_execute(const Scene &scene,
|
|
const RenderData &render_data,
|
|
const bNodeTree &node_tree,
|
|
const bool use_file_output,
|
|
const char *view_name,
|
|
blender::realtime_compositor::RenderContext *render_context)
|
|
{
|
|
std::unique_lock lock(gpu_compositor_mutex);
|
|
|
|
blender::render::ContextInputData input_data(
|
|
scene, render_data, node_tree, use_file_output, view_name, render_context);
|
|
|
|
if (gpu_compositor == nullptr) {
|
|
gpu_compositor = new blender::render::RealtimeCompositor(*this, input_data);
|
|
}
|
|
|
|
gpu_compositor->execute(input_data);
|
|
}
|
|
|
|
void Render::compositor_free()
|
|
{
|
|
std::unique_lock lock(gpu_compositor_mutex);
|
|
|
|
if (gpu_compositor != nullptr) {
|
|
delete gpu_compositor;
|
|
gpu_compositor = nullptr;
|
|
}
|
|
}
|
|
|
|
void RE_compositor_execute(Render &render,
|
|
const Scene &scene,
|
|
const RenderData &render_data,
|
|
const bNodeTree &node_tree,
|
|
const bool use_file_output,
|
|
const char *view_name,
|
|
blender::realtime_compositor::RenderContext *render_context)
|
|
{
|
|
render.compositor_execute(
|
|
scene, render_data, node_tree, use_file_output, view_name, render_context);
|
|
}
|
|
|
|
void RE_compositor_free(Render &render)
|
|
{
|
|
render.compositor_free();
|
|
}
|