Compositor: Add playback cache

This patch adds support for frame animation playback caching in the
compositor. There is now a Frame Cache option in the side bar, when
enabled, the compositor will cache its final viewer result across
frames. The cache is drawn in the timeline editor with a new display
option for that cache. The cache is invalidated on any change that
affects the node tree. The cache respects the memory cache limit and
will evict cached frames in the limit was surpassed. This caching
mechanism only concerns the interactive compositor, not the viewport
compositor or the VSE compositors.

A few changes were made to support this patch:

- A new DEG_id_is_user_modified query function was added to detect if an
  ID changed due to user input or not. This is used to do proper cached
  invalidation. Thanks to Brecht for the implementation.
- The compositor job now internally suspends animation playback until
  the compositor is done to make sure the entire frame range is computed
  and cached. The previous behavior of frame dropping can still be
  achieved using the playback frame dropping option.

Pull Request: https://projects.blender.org/blender/blender/pulls/159902
This commit is contained in:
Omar Emara 2026-06-18 07:40:41 +02:00 • committed by Omar Emara
parent ac72097b12
commit 0f5669bbfb
18 changed files with 425 additions and 39 deletions

View file

@ -522,6 +522,7 @@ class DOPESHEET_MT_cache(Menu):
col.prop(st, "cache_softbody")
col.prop(st, "cache_particles")
col.prop(st, "cache_cloth")
col.prop(st, "cache_compositor")
col.prop(st, "cache_simulation_nodes")
col.prop(st, "cache_smoke")
col.prop(st, "cache_dynamicpaint")

View file

@ -971,6 +971,10 @@ class NODE_PT_quality(Panel):
if rd.compositor_device == 'GPU':
col.prop(rd, "compositor_precision", text="Precision")
if snode.node_tree_sub_type == 'SCENE':
col = layout.column(heading="Cache", align=True)
col.prop(rd, "use_compositor_frames_cache", text="Frames")
class NODE_PT_overlay(Panel):
bl_space_type = 'NODE_EDITOR'

View file

@ -30,7 +30,7 @@ namespace blender {
/* Blender file format version. */
#define BLENDER_FILE_VERSION BLENDER_VERSION
#define BLENDER_FILE_SUBVERSION 3
#define BLENDER_FILE_SUBVERSION 4
/* Minimum Blender version that supports reading file written with the current
* version. Older Blender versions will test this and cancel loading the file, showing a warning to

View file

@ -10,18 +10,72 @@
#include <string>
#include "BLI_index_range.hh"
#include "BLI_map.hh"
#include "BLI_mutex.hh"
#include "BLI_set.hh"
#include "BLI_vector.hh"
namespace blender {
struct Scene;
struct ViewLayer;
struct ImBuf;
struct bContext;
struct DepsNodeHandle;
struct bNodeTree;
namespace bke::compositor {
struct Cache {
struct FrameKey {
int frame_number = 0;
int view_identifier = 0;
uint64_t hash() const
{
return get_default_hash(frame_number, view_identifier);
}
friend bool operator==(const FrameKey &a, const FrameKey &b) = default;
};
private:
/* A cache of final interactive compositor results across frames. */
Map<FrameKey, ImBuf *> frames_;
/* A mutex for accessing frames_. The frames cache is intrinsically thread-safe since all access
* happen in the same interactive compositor job, except for drawing cache overlays since it can
* happen simultaneously while the job is running, that's why the mutex is needed. */
Mutex frames_mutex_;
public:
/* Clear all caches. */
~Cache();
/* Get the frame cache corresponding to the given frame number and view. */
const ImBuf *get_frame(int frame_number, int view_identifier);
/* Add a new frame cache entry. If the new entry would surpass the memory cache limit, frames
* will be evicted to make room. */
void add_frame(int frame_number, int view_identifier, ImBuf *image_buffer);
/* Clears the frames cache. */
void clear_frames();
/* Computes a list of every contiguous segment of cached frames. Can be used to draw which frame
* ranges are cached. */
Vector<IndexRange> compute_frame_ranges();
private:
/* Delete one entry from the frames cache given the current frame number. If a cached frame exist
* before the current frame, the furthest one will be removed, otherwise, the furthest cached
* frame after the current frame will be removed. */
void evict_frame(int current_frame_number);
/* Computes the total size of the cache in bytes. */
int64_t size();
};
/* Get the set of all passes used by the compositor for the given view layer, identified by their
* pass names. This might be a superset of the passes actually supported by the render engine, in
* which case, the compositor will return an invalid output and issue a warning. */

View file

@ -10,6 +10,7 @@
#include <memory>
#include "BKE_compositor.hh"
#include "BKE_sound_types.hh"
#include "BLI_set.hh"
@ -30,6 +31,8 @@ class CompositorRuntime {
public:
/* A nodes log of the last compositor evaluation. */
std::unique_ptr<nodes::eval_log::NodesEvalLog> nodes_evaluation_log;
/* Cached data for the interactive compositor. */
bke::compositor::Cache cache;
/* A dependency graph used for interactive compositing. This is initialized the first time it is
* needed, and then kept persistent for the lifetime of the scene. This is done to allow the
* compositor to track changes to resources its uses as well as reduce the overhead of creating

View file

@ -2,6 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include <string>
#include <fmt/format.h>
@ -31,10 +32,116 @@
#include "DNA_view3d_types.h"
#include "DNA_windowmanager_types.h"
#include "IMB_imbuf.hh"
#include "NOD_dependencies.hh"
namespace blender::bke::compositor {
Cache::~Cache()
{
this->clear_frames();
}
const ImBuf *Cache::get_frame(const int frame_number, const int view_identifier)
{
std::scoped_lock lock{frames_mutex_};
return this->frames_.lookup_try(FrameKey(frame_number, view_identifier)).value_or(nullptr);
}
void Cache::add_frame(const int frame_number, const int view_identifier, ImBuf *image_buffer)
{
std::scoped_lock lock{frames_mutex_};
/* First evict frames if needed to maintain the memory cache limit. In almost all cases, the
* while loop will run exactly once, since the images in the cache will almost always have the
* same size, so one goes out, one goes in. So we needn't worry about performance. */
const int64_t cache_limit = size_t(U.memcachelimit) * 1024 * 1024;
const int64_t image_size = IMB_get_size_in_memory(image_buffer);
while (!this->frames_.is_empty() && this->size() + image_size > cache_limit) {
this->evict_frame(frame_number);
}
this->frames_.add_new(FrameKey(frame_number, view_identifier), image_buffer);
}
void Cache::clear_frames()
{
std::scoped_lock lock{frames_mutex_};
for (ImBuf *image_buffer : this->frames_.values()) {
IMB_freeImBuf(image_buffer);
}
this->frames_.clear();
}
Vector<IndexRange> Cache::compute_frame_ranges()
{
/* Compute a sorted vector of all cached frames. */
VectorSet<int> frame_numbers_set;
{
std::scoped_lock lock{frames_mutex_};
frame_numbers_set.reserve(this->frames_.size());
for (const FrameKey &key : this->frames_.keys()) {
frame_numbers_set.add(key.frame_number);
}
}
Vector<int> frame_numbers = frame_numbers_set.extract_vector();
std::ranges::sort(frame_numbers);
Vector<IndexRange> frame_ranges;
for (const int frame : frame_numbers) {
/* We start a new range by appending a singleton range of the current frame, either because
* this is the first range or because the last range will not be contiguous with the current
* frame. */
if (frame_ranges.is_empty() || frame - frame_ranges.last().last() > 1) {
frame_ranges.append(IndexRange(frame, 1));
}
else {
/* Otherwise, the frame is contiguous with the last range, so we just grow its size by 1. */
frame_ranges.last() = IndexRange(frame_ranges.last().start(),
frame_ranges.last().size() + 1);
}
}
return frame_ranges;
}
void Cache::evict_frame(const int current_frame_number)
{
if (this->frames_.is_empty()) {
return;
}
/* Find the keys with the maximum and minimum frame numbers. */
FrameKey minimum_key = FrameKey(std::numeric_limits<int>::max());
FrameKey maximum_key = FrameKey(std::numeric_limits<int>::lowest());
for (const FrameKey &key : this->frames_.keys()) {
if (key.frame_number < minimum_key.frame_number) {
minimum_key = key;
}
if (key.frame_number > maximum_key.frame_number) {
maximum_key = key;
}
}
/* Prioritize evicting frames that are behind the current frame and are furthest from it. */
if (minimum_key.frame_number < current_frame_number) {
IMB_freeImBuf(this->frames_.pop(minimum_key));
return;
}
/* Otherwise, evict the frame that is after the current frame and is furthest from it. */
IMB_freeImBuf(this->frames_.pop(maximum_key));
}
int64_t Cache::size()
{
int64_t size = 0;
for (ImBuf *image_buffer : this->frames_.values()) {
size += IMB_get_size_in_memory(image_buffer);
}
return size;
}
/* Adds the pass names of the passes used by the given Render Layer node to the given used passes.
* This essentially adds the pass names of the outputs that are logically linked. */
static void add_passes_used_by_render_layer_node(const bNode *node, Set<std::string> &used_passes)

View file

@ -81,6 +81,19 @@ void blo_do_versions_530(FileData * /*fd*/, Library * /*lib*/, Main *bmain)
FOREACH_NODETREE_END;
}
if (!MAIN_VERSION_FILE_ATLEAST(bmain, 503, 4)) {
for (bScreen &screen : bmain->screens) {
for (ScrArea &area : screen.areabase) {
for (SpaceLink &sl : area.spacedata) {
if (sl.spacetype == SPACE_ACTION) {
SpaceAction *saction = reinterpret_cast<SpaceAction *>(&sl);
saction->cache_display |= TIME_CACHE_COMPOSITOR;
}
}
}
}
}
/**
* Always bump subversion in BKE_blender_version.h when adding versioning
* code here, and wrap it inside a MAIN_VERSION_FILE_ATLEAST check.

View file

@ -65,6 +65,9 @@ float DEG_get_ctime(const Depsgraph *graph);
bool DEG_id_type_updated(const Depsgraph *depsgraph, short id_type);
bool DEG_id_type_any_updated(const Depsgraph *depsgraph);
/** Check if given ID was updated due to user input. */
bool DEG_id_is_user_modified(const Depsgraph *graph, const ID *id);
/** Check if given ID type is present in the depsgraph */
bool DEG_id_type_any_exists(const Depsgraph *depsgraph, short id_type);

View file

@ -128,6 +128,16 @@ bool DEG_id_type_any_updated(const Depsgraph *graph)
return false;
}
bool DEG_id_is_user_modified(const Depsgraph *graph, const ID *id)
{
const deg::Depsgraph *deg_graph = reinterpret_cast<const deg::Depsgraph *>(graph);
const deg::IDNode *id_node = deg_graph->find_id_node(deg::get_original_id(id));
if (id_node == nullptr) {
return false;
}
return id_node->is_user_modified;
}
bool DEG_id_type_any_exists(const Depsgraph *depsgraph, short id_type)
{
const deg::Depsgraph *deg_graph = reinterpret_cast<const deg::Depsgraph *>(depsgraph);

View file

@ -38,6 +38,7 @@
#include "BKE_node_tree_update.hh"
#include "BKE_paint.hh"
#include "BKE_scene.hh"
#include "BKE_scene_runtime.hh"
#include "RE_engine.h"
#include "RE_pipeline.h"
@ -114,6 +115,10 @@ static void update_compositor(const DEGEditorUpdateContext *update_context)
}
if (node_tree->id.recalc & ID_RECALC_NTREE_OUTPUT) {
if (DEG_id_is_user_modified(update_context->depsgraph, &node_tree->id)) {
update_context->scene->runtime->compositor.cache.clear_frames();
}
ED_node_compositor_job(
update_context->bmain, update_context->scene, update_context->view_layer);
}

View file

@ -27,6 +27,7 @@
#include "BKE_bake_geometry_nodes_modifier.hh"
#include "BKE_pointcache.h"
#include "BKE_scene_runtime.hh"
#include "ANIM_action.hh"
@ -837,6 +838,30 @@ static void timeline_cache_draw_geometry_nodes(const Span<CacheRange> cache_rang
*y_offset += max_used_height * 2;
}
/* Draw the interactive compositor playback cache markers. */
static void timeline_cache_draw_compositor(const Vector<IndexRange> cached_frame_ranges,
const float y_offset,
const float line_height,
const uint pos_id)
{
GPU_matrix_push();
GPU_matrix_translate_2f(0.0, float(V2D_SCROLL_HANDLE_HEIGHT) + y_offset);
GPU_matrix_scale_2f(1.0, line_height);
ColorTheme4f color;
ui::theme::get_color_4fv(TH_SIMULATED_FRAMES, color);
for (const IndexRange &range : cached_frame_ranges) {
immUniform4fv("color1", color);
immUniform4fv("color2", color);
immBeginAtMost(GPU_PRIM_TRIS, 6);
immRectf_fast(pos_id, range.start(), 0.0f, range.last() + 1.0f, 1.0f);
immEnd();
}
GPU_matrix_pop();
}
void timeline_draw_cache(const SpaceAction *saction, const Object *ob, const Scene *scene)
{
if ((saction->cache_display & TIME_CACHE_DISPLAY) == 0 || ob == nullptr) {
@ -922,6 +947,14 @@ void timeline_draw_cache(const SpaceAction *saction, const Object *ob, const Sce
timeline_cache_draw_geometry_nodes(
cache_ranges, all_simulations_baked, &y_offset, cache_draw_height, pos_id);
}
if (saction->cache_display & TIME_CACHE_COMPOSITOR) {
const Vector<IndexRange> cached_frame_ranges =
scene->runtime->compositor.cache.compute_frame_ranges();
if (!cached_frame_ranges.is_empty()) {
timeline_cache_draw_compositor(cached_frame_ranges, y_offset, cache_draw_height, pos_id);
y_offset += cache_draw_height;
}
}
GPU_blend(GPU_BLEND_NONE);
immUnbindProgram();

View file

@ -84,7 +84,8 @@ static SpaceLink *action_create(const ScrArea *area, const Scene *scene)
saction->cache_display = TIME_CACHE_DISPLAY | TIME_CACHE_SOFTBODY | TIME_CACHE_PARTICLES |
TIME_CACHE_CLOTH | TIME_CACHE_SMOKE | TIME_CACHE_DYNAMICPAINT |
TIME_CACHE_RIGIDBODY | TIME_CACHE_SIMULATION_NODES;
TIME_CACHE_RIGIDBODY | TIME_CACHE_SIMULATION_NODES |
TIME_CACHE_COMPOSITOR;
saction->overlays.flag |= (ADS_OVERLAY_SHOW_OVERLAYS | ADS_SHOW_SCENE_STRIP_FRAME_RANGE);

View file

@ -37,15 +37,25 @@
namespace blender {
struct CompositorJob {
wmWindowManager *window_manager;
Main *bmain;
Scene *scene;
ViewLayer *view_layer;
bNodeTree *evaluated_node_tree;
Render *render;
compositor::NodeGroupOutputTypes needed_outputs;
bool is_animation_playing;
};
/* Suspend or resume animation playback if animation is playing. */
static void set_animation_playback(wmWindowManager *window_manager, const bool enabled)
{
wmWindow *animation_playback_window = ED_window_animation_playing_no_scrub(window_manager);
if (animation_playback_window) {
bScreen *screen = WM_window_get_active_screen(animation_playback_window);
WM_event_timer_sleep(window_manager, animation_playback_window, screen->animtimer, !enabled);
}
}
static void compositor_job_init(void *compositor_job_data)
{
CompositorJob *compositor_job = static_cast<CompositorJob *>(compositor_job_data);
@ -82,26 +92,19 @@ static void compositor_job_init(void *compositor_job_data)
RE_display_ensure_gpu_context(compositor_job->render);
IMB_ensure_gpu_context();
}
/* Suspend animation playback (if any) until the compositor is done to allow frames to be fully
* processed. */
set_animation_playback(compositor_job->window_manager, false);
}
static void compositor_job_start(void *compositor_job_data, wmJobWorkerStatus *worker_status)
{
CompositorJob *compositor_job = static_cast<CompositorJob *>(compositor_job_data);
/* If animation is playing, do not respect the job worker stop status, because if the job for the
* current frame did not finish before the next frame's job is scheduled, it will be stopped in
* favor of the new frame, and this will likely happen for all future frame jobs so we will be
* essentially doing nothing. So we just prefer to finish the job at hand and ignore the future
* jobs. This will appear to be frame-dropping for the user. */
if (compositor_job->is_animation_playing) {
RE_test_break_cb(
compositor_job->render, nullptr, [](void * /*handle*/) -> bool { return G.is_break; });
}
else {
RE_test_break_cb(compositor_job->render, &worker_status->stop, [](void *should_stop) -> bool {
return *static_cast<bool *>(should_stop) || G.is_break;
});
}
RE_test_break_cb(compositor_job->render, &worker_status->stop, [](void *should_stop) -> bool {
return *static_cast<bool *>(should_stop) || G.is_break;
});
BKE_callback_exec_id(
compositor_job->bmain, &compositor_job->scene->id, BKE_CB_EVT_COMPOSITE_PRE);
@ -147,22 +150,20 @@ static void compositor_job_complete(void *compositor_job_data)
evaluated_scene->runtime->compositor.nodes_evaluation_log);
WM_main_add_notifier(NC_SCENE | ND_COMPO_RESULT, nullptr);
/* Resume animation playback (if any) after the compositor is done. */
set_animation_playback(compositor_job->window_manager, true);
}
static void compositor_job_cancel(void *compositor_job_data)
{
CompositorJob *compositor_job = static_cast<CompositorJob *>(compositor_job_data);
/* If animation is playing, jobs can only be canceled by the user, that is, through G.is_break,
* so if we are not breaked, consider the job to be complete. See comment in compositor_job_start
* breaking callbacks. */
if (compositor_job->is_animation_playing && !G.is_break) {
compositor_job_complete(compositor_job);
return;
}
Scene *scene = compositor_job->scene;
BKE_callback_exec_id(compositor_job->bmain, &scene->id, BKE_CB_EVT_COMPOSITE_CANCEL);
/* Resume animation playback (if any) after the compositor is done. */
set_animation_playback(compositor_job->window_manager, true);
}
static void compositor_job_free(void *compositor_job_data)
@ -291,11 +292,11 @@ void ED_node_compositor_job(Main *bmain, Scene *scene, ViewLayer *view_layer)
WM_JOB_TYPE_COMPOSITE);
CompositorJob *compositor_job = MEM_new<CompositorJob>("Compositor Job");
compositor_job->window_manager = window_manager;
compositor_job->bmain = bmain;
compositor_job->scene = scene;
compositor_job->view_layer = view_layer;
compositor_job->needed_outputs = needed_outputs;
compositor_job->is_animation_playing = ED_window_animation_playing_no_scrub(window_manager);
WM_jobs_customdata_set(job, compositor_job, compositor_job_free);
WM_jobs_timer(job, 0.1, 0, 0);

View file

@ -564,7 +564,7 @@ enum DNA_DEPRECATED eAnimEdit_AutoSnap : int {
};
/* SAction->cache_display */
enum eTimeline_Cache_Flag : char {
enum eTimeline_Cache_Flag : uint16_t {
TIME_CACHE_DISPLAY = (1 << 0),
TIME_CACHE_SOFTBODY = (1 << 1),
TIME_CACHE_PARTICLES = (1 << 2),
@ -572,7 +572,8 @@ enum eTimeline_Cache_Flag : char {
TIME_CACHE_SMOKE = (1 << 4),
TIME_CACHE_DYNAMICPAINT = (1 << 5),
TIME_CACHE_RIGIDBODY = (1 << 6),
TIME_CACHE_SIMULATION_NODES = static_cast<char>(1 << 7),
TIME_CACHE_SIMULATION_NODES = (1 << 7),
TIME_CACHE_COMPOSITOR = (1 << 8),
};
ENUM_OPERATORS(eTimeline_Cache_Flag)
@ -1194,8 +1195,9 @@ struct SpaceAction {
char mode_prev = 0;
/* Snapping now lives on the Scene. */
DNA_DEPRECATED char autosnap = 0;
char _pad1 = {};
eTimeline_Cache_Flag cache_display = {};
char _pad1[6] = {};
char _pad2[4] = {};
SpaceActionOverlays overlays;

View file

@ -662,6 +662,13 @@ enum eCompositorDevice : int {
SCE_COMPOSITOR_DEVICE_GPU = 1,
};
/** #RenderData::cache_flags */
enum eCompositorCacheFlags : uint8_t {
SCE_COMPOSITOR_CACHE_NONE = 0,
SCE_COMPOSITOR_CACHE_FRAMES = (1 << 0),
};
ENUM_OPERATORS(eCompositorCacheFlags);
/** #RenderData::compositor_precision */
enum eCompositorPrecision : int {
SCE_COMPOSITOR_PRECISION_AUTO = 0,
@ -1037,6 +1044,11 @@ struct RenderData {
/** Device to use for compositor engine. */
eCompositorDevice compositor_device = SCE_COMPOSITOR_DEVICE_GPU;
/** Cache options for the interactive compositor. */
eCompositorCacheFlags compositor_cache_flags = SCE_COMPOSITOR_CACHE_FRAMES;
char _pad10[3] = {};
/** Precision used by the GPU execution of the compositor tree. */
eCompositorPrecision compositor_precision = SCE_COMPOSITOR_PRECISION_AUTO;
@ -1050,7 +1062,6 @@ struct RenderData {
/** Frames to jump manually. */
float time_jump_delta = 1.0;
int time_jump_unit = 1;
char _pad10[4] = {};
};
/** \} */

View file

@ -7792,6 +7792,14 @@ static void rna_def_scene_render_data(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Compositor Device", "Set how compositing is executed");
RNA_def_property_update(prop, NC_NODE | ND_DISPLAY, "rna_Scene_compositor_update");
prop = RNA_def_property(srna, "use_compositor_frames_cache", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(
prop, nullptr, "compositor_cache_flags", SCE_COMPOSITOR_CACHE_FRAMES);
RNA_def_property_ui_text(prop,
"Compositor Frames Cache",
"Cache the result of the interactive compositor across frames");
RNA_def_property_update(prop, NC_NODE | ND_DISPLAY, "rna_Scene_compositor_update");
prop = RNA_def_property(srna, "compositor_precision", PROP_ENUM, PROP_NONE);
RNA_def_property_enum_sdna(prop, nullptr, "compositor_precision");
RNA_def_property_enum_items(prop, compositor_precision_items);

View file

@ -7303,6 +7303,11 @@ static void rna_def_space_dopesheet(BlenderRNA *brna)
RNA_def_property_ui_text(prop, "Smoke", "Show the active object's smoke cache");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_TIME, nullptr);
prop = RNA_def_property(srna, "cache_compositor", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "cache_display", TIME_CACHE_COMPOSITOR);
RNA_def_property_ui_text(prop, "Compositor", "Show the interactive compositor playback cache");
RNA_def_property_update(prop, NC_SPACE | ND_SPACE_TIME, nullptr);
prop = RNA_def_property(srna, "cache_simulation_nodes", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "cache_display", TIME_CACHE_SIMULATION_NODES);
RNA_def_property_ui_text(

View file

@ -26,8 +26,11 @@
#include "DRW_engine.hh"
#include "DRW_render.hh"
#include "IMB_colormanagement.hh"
#include "IMB_imbuf.hh"
#include "DEG_depsgraph_query.hh"
#include "COM_context.hh"
#include "COM_conversion_operation.hh"
#include "COM_domain.hh"
@ -36,6 +39,7 @@
#include "COM_render_context.hh"
#include "COM_result.hh"
#include "NOD_dependencies.hh"
#include "NOD_eval_log.hh"
#include "RE_compositor.hh"
@ -226,20 +230,36 @@ class Context : public compositor::Context {
BKE_image_partial_update_mark_full_update(image);
}
bool should_cache_viewer_result()
{
/* Caching disabled. */
if (!flag_is_set(this->get_render_data().compositor_cache_flags, SCE_COMPOSITOR_CACHE_FRAMES))
{
return false;
}
/* Not an interactive compositor, so no need to cache. */
if (this->render_context()) {
return false;
}
/* Node tree is not time depend, so no need to cache. */
const bNodeTree *original_node_tree = DEG_get_original(input_data_.node_tree);
if (!original_node_tree->runtime->eval_dependencies->time_dependent) {
return false;
}
return true;
}
void write_viewer_image(const compositor::Result &viewer_result)
{
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_COMPOSITE, "Viewer Node");
if (viewer_result.meta_data.is_non_color_data) {
image->flag &= ~IMA_VIEW_AS_RENDER;
}
else {
image->flag |= IMA_VIEW_AS_RENDER;
}
ImageUser image_user = {nullptr};
image_user.multi_index = BKE_scene_multiview_view_id_get(input_data_.render_data,
input_data_.view_name.c_str());
const int view_identifier = BKE_scene_multiview_view_id_get(input_data_.render_data,
input_data_.view_name.c_str());
image_user.multi_index = view_identifier;
if (BKE_scene_multiview_is_render_view_first(input_data_.render_data,
input_data_.view_name.c_str()))
@ -328,6 +348,39 @@ class Context : public compositor::Context {
image_buffer->flags &= ~ImBufFlags::HasDisplayWindow;
}
if (viewer_result.meta_data.is_non_color_data) {
image->flag &= ~IMA_VIEW_AS_RENDER;
image_buffer->float_buffer.colorspace = IMB_colormanagement_space_get_named(
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_DATA));
}
else {
image->flag |= IMA_VIEW_AS_RENDER;
image_buffer->float_buffer.colorspace = IMB_colormanagement_space_get_named(
IMB_colormanagement_role_colorspace_name_get(COLOR_ROLE_SCENE_LINEAR));
}
if (this->should_cache_viewer_result()) {
/* Duplicate the viewer image buffer to store in the cache, making sure to manually
* duplicate the GPU texture since it is not done in IMB_dupImBuf. */
ImBuf *cached_buffer = IMB_dupImBuf(image_buffer);
if (this->use_gpu()) {
gpu::Texture *texture = GPU_texture_create_2d(
__func__,
GPU_texture_width(image_buffer->gpu.texture),
GPU_texture_height(image_buffer->gpu.texture),
1,
GPU_texture_format(image_buffer->gpu.texture),
GPU_TEXTURE_USAGE_GENERAL,
nullptr);
GPU_texture_copy(texture, image_buffer->gpu.texture);
IMB_assign_gpu_texture(cached_buffer, texture);
}
const Scene *original_scene = DEG_get_original(&this->get_scene());
original_scene->runtime->compositor.cache.add_frame(
this->get_frame_number(), view_identifier, cached_buffer);
}
BKE_image_partial_update_mark_full_update(image);
BKE_image_release_ibuf(image, image_buffer, lock);
BLI_thread_unlock(LOCK_DRAW_IMAGE);
@ -591,8 +644,80 @@ class Context : public compositor::Context {
return input_data_.render->display->test_break();
}
/* Checks if a cached viewer result exists for the current frame. If no cache is found, false is
* returned and nothing is done. If the cache exists, write it to the viewer image and return
* true. */
bool write_frame_cache()
{
const Scene *original_scene = DEG_get_original(&this->get_scene());
const int view_identifier = BKE_scene_multiview_view_id_get(input_data_.render_data,
input_data_.view_name.c_str());
const ImBuf *cached_buffer = original_scene->runtime->compositor.cache.get_frame(
this->get_frame_number(), view_identifier);
if (!cached_buffer) {
return false;
}
Image *image = BKE_image_ensure_viewer(G.main, IMA_TYPE_COMPOSITE, "Viewer Node");
ImageUser image_user = {nullptr};
image_user.multi_index = view_identifier;
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_gpu(image, &image_user, &lock);
image_buffer->x = cached_buffer->x;
image_buffer->y = cached_buffer->y;
copy_v2_v2_int(image_buffer->display_size, cached_buffer->display_size);
copy_v2_v2_int(image_buffer->data_offset, cached_buffer->data_offset);
copy_v2_v2_int(image_buffer->display_offset, cached_buffer->display_offset);
image_buffer->color_mode = cached_buffer->color_mode;
image_buffer->channels = cached_buffer->channels;
image_buffer->flags = cached_buffer->flags;
image_buffer->byte_buffer = cached_buffer->byte_buffer;
image_buffer->float_buffer = cached_buffer->float_buffer;
IMB_free_gpu_textures(image_buffer);
if (cached_buffer->gpu.texture) {
gpu::Texture *texture = GPU_texture_create_2d(__func__,
GPU_texture_width(cached_buffer->gpu.texture),
GPU_texture_height(cached_buffer->gpu.texture),
1,
GPU_texture_format(cached_buffer->gpu.texture),
GPU_TEXTURE_USAGE_GENERAL,
nullptr);
GPU_texture_copy(texture, cached_buffer->gpu.texture);
IMB_assign_gpu_texture(image_buffer, texture);
}
image_buffer->userflags = cached_buffer->userflags;
if (IMB_colormanagement_space_is_data(cached_buffer->float_buffer.colorspace)) {
image->flag &= ~IMA_VIEW_AS_RENDER;
}
else {
image->flag |= IMA_VIEW_AS_RENDER;
}
BKE_image_partial_update_mark_full_update(image);
BKE_image_release_ibuf(image, image_buffer, lock);
BLI_thread_unlock(LOCK_DRAW_IMAGE);
return true;
}
void evaluate()
{
if (this->write_frame_cache()) {
return;
}
/* Reset log before evaluation. */
this->get_scene().runtime->compositor.nodes_evaluation_log =
std::make_unique<nodes::eval_log::NodesEvalLog>();