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,42 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
set(INC
..
)
set(INC_SYS
)
set(SRC
buffers.cpp
cache_eviction.cpp
denoising.cpp
display_driver.cpp
merge.cpp
session.cpp
tile.cpp
)
set(SRC_HEADERS
buffers.h
cache_eviction.h
denoising.h
display_driver.h
merge.h
output_driver.h
session.h
tile.h
)
set(LIB
PUBLIC cycles_device
PUBLIC cycles_integrator
PUBLIC cycles_util
)
include_directories(${INC})
include_directories(SYSTEM ${INC_SYS})
cycles_add_library(cycles_session "${LIB}" ${SRC} ${SRC_HEADERS})

View File

@@ -0,0 +1,372 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include <cstdlib>
#include "device/device.h"
#include "session/buffers.h"
#include "util/log.h"
CCL_NAMESPACE_BEGIN
/* --------------------------------------------------------------------
* Convert part information to an index of `BufferParams::pass_offset_`.
*/
static int pass_type_mode_to_index(PassType pass_type, PassMode mode)
{
int index = static_cast<int>(pass_type) * 2;
if (mode == PassMode::DENOISED) {
++index;
}
return index;
}
static int pass_to_index(const BufferPass &pass)
{
return pass_type_mode_to_index(pass.type, pass.mode);
}
/* --------------------------------------------------------------------
* Buffer pass.
*/
NODE_DEFINE(BufferPass)
{
NodeType *type = NodeType::add("buffer_pass", create);
const NodeEnum *pass_type_enum = Pass::get_type_enum();
const NodeEnum *pass_mode_enum = Pass::get_mode_enum();
SOCKET_ENUM(type, "Type", *pass_type_enum, PASS_COMBINED);
SOCKET_ENUM(mode, "Mode", *pass_mode_enum, static_cast<int>(PassMode::DENOISED));
SOCKET_STRING(name, "Name", ustring());
SOCKET_BOOLEAN(include_albedo, "Include Albedo", false);
SOCKET_STRING(lightgroup, "Light Group", ustring());
SOCKET_INT(offset, "Offset", -1);
return type;
}
BufferPass::BufferPass() : Node(get_node_type()) {}
BufferPass::BufferPass(const Pass *scene_pass)
: Node(get_node_type()),
type(scene_pass->get_type()),
mode(scene_pass->get_mode()),
name(scene_pass->get_name()),
include_albedo(scene_pass->get_include_albedo()),
lightgroup(scene_pass->get_lightgroup())
{
}
PassInfo BufferPass::get_info() const
{
return Pass::get_info(type, mode, include_albedo, !lightgroup.empty());
}
/* --------------------------------------------------------------------
* Buffer Parameters.
*/
NODE_DEFINE(BufferParams)
{
NodeType *type = NodeType::add("buffer_params", create);
SOCKET_INT(width, "Width", 0);
SOCKET_INT(height, "Height", 0);
SOCKET_INT(window_x, "Window X", 0);
SOCKET_INT(window_y, "Window Y", 0);
SOCKET_INT(window_width, "Window Width", 0);
SOCKET_INT(window_height, "Window Height", 0);
SOCKET_INT(full_x, "Full X", 0);
SOCKET_INT(full_y, "Full Y", 0);
SOCKET_INT(full_width, "Full Width", 0);
SOCKET_INT(full_height, "Full Height", 0);
SOCKET_STRING(layer, "Layer", ustring());
SOCKET_STRING(view, "View", ustring());
SOCKET_INT(samples, "Samples", 0);
SOCKET_FLOAT(exposure, "Exposure", 1.0f);
SOCKET_BOOLEAN(use_approximate_shadow_catcher, "Use Approximate Shadow Catcher", false);
SOCKET_BOOLEAN(use_transparent_background, "Transparent Background", false);
/* Notes:
* - Skip passes since they do not follow typical container socket definition.
* Might look into covering those as a socket in the future.
*
* - Skip offset, stride, and pass stride since those can be delivered from the passes and
* rest of the sockets. */
return type;
}
BufferParams::BufferParams() : Node(get_node_type())
{
reset_pass_offset();
}
void BufferParams::update_passes()
{
update_offset_stride();
reset_pass_offset();
pass_stride = 0;
for (const BufferPass &pass : passes) {
if (pass.offset != PASS_UNUSED) {
const int index = pass_to_index(pass);
if (pass_offset_[index] == PASS_UNUSED) {
pass_offset_[index] = pass_stride;
}
pass_stride += pass.get_info().num_components;
}
}
}
void BufferParams::update_passes(const unique_ptr_vector<Pass> &scene_passes)
{
passes.clear();
pass_stride = 0;
for (const Pass *scene_pass : scene_passes) {
BufferPass buffer_pass(scene_pass);
if (scene_pass->is_written()) {
buffer_pass.offset = pass_stride;
pass_stride += scene_pass->get_info().num_components;
}
else {
buffer_pass.offset = PASS_UNUSED;
}
passes.emplace_back(std::move(buffer_pass));
}
update_passes();
}
void BufferParams::reset_pass_offset()
{
for (int i = 0; i < kNumPassOffsets; ++i) {
pass_offset_[i] = PASS_UNUSED;
}
}
int BufferParams::get_pass_offset(PassType pass_type, PassMode mode) const
{
if (pass_type == PASS_NONE) {
return PASS_UNUSED;
}
const int index = pass_type_mode_to_index(pass_type, mode);
return pass_offset_[index];
}
const BufferPass *BufferParams::find_pass(string_view name) const
{
for (const BufferPass &pass : passes) {
if (pass.name == name) {
return &pass;
}
}
return nullptr;
}
const BufferPass *BufferParams::find_pass(PassType type, PassMode mode) const
{
for (const BufferPass &pass : passes) {
if (pass.type == type && pass.mode == mode) {
return &pass;
}
}
return nullptr;
}
const BufferPass *BufferParams::get_actual_display_pass(PassType type, PassMode mode) const
{
const BufferPass *pass = find_pass(type, mode);
return get_actual_display_pass(pass);
}
const BufferPass *BufferParams::get_actual_display_pass(const BufferPass *pass) const
{
if (!pass) {
return nullptr;
}
if (pass->type == PASS_COMBINED && pass->lightgroup.empty()) {
const BufferPass *shadow_catcher_matte_pass = find_pass(PASS_SHADOW_CATCHER_MATTE, pass->mode);
if (shadow_catcher_matte_pass) {
pass = shadow_catcher_matte_pass;
}
}
return pass;
}
void BufferParams::update_offset_stride()
{
offset = -(full_x + full_y * width);
stride = width;
}
bool BufferParams::modified(const BufferParams &other) const
{
if (width != other.width || height != other.height) {
return true;
}
if (full_x != other.full_x || full_y != other.full_y || full_width != other.full_width ||
full_height != other.full_height)
{
return true;
}
if (window_x != other.window_x || window_y != other.window_y ||
window_width != other.window_width || window_height != other.window_height)
{
return true;
}
if (offset != other.offset || stride != other.stride || pass_stride != other.pass_stride) {
return true;
}
if (layer != other.layer || view != other.view) {
return true;
}
if (exposure != other.exposure ||
use_approximate_shadow_catcher != other.use_approximate_shadow_catcher ||
use_transparent_background != other.use_transparent_background)
{
return true;
}
return !(passes == other.passes);
}
/* --------------------------------------------------------------------
* Render Buffers.
*/
RenderBuffers::RenderBuffers(Device *device) : buffer(device, "RenderBuffers", MEM_READ_WRITE) {}
RenderBuffers::~RenderBuffers()
{
buffer.free();
}
void RenderBuffers::reset(const BufferParams &params_)
{
DCHECK(params_.pass_stride != -1);
params = params_;
/* re-allocate buffer */
buffer.alloc(params.width * params.pass_stride, params.height);
}
void RenderBuffers::zero()
{
buffer.zero_to_device();
}
bool RenderBuffers::copy_from_device()
{
DCHECK(params.pass_stride != -1);
if (!buffer.device_pointer) {
return false;
}
buffer.copy_from_device(0, params.width * params.pass_stride, params.height);
return true;
}
void RenderBuffers::copy_to_device()
{
buffer.copy_to_device();
}
void render_buffers_host_copy_denoised(RenderBuffers *dst,
const BufferParams &dst_params,
const RenderBuffers *src,
const BufferParams &src_params,
const size_t src_offset)
{
DCHECK_EQ(dst_params.width, src_params.width);
/* TODO(sergey): More sanity checks to avoid buffer overrun. */
/* Create a map of pass offsets to be copied.
* Assume offsets are different to allow copying passes between buffers with different set of
* passes. */
struct {
int dst_offset;
int src_offset;
} pass_offsets[PASS_NUM];
int num_passes = 0;
for (int i = 0; i < PASS_NUM; ++i) {
const PassType pass_type = static_cast<PassType>(i);
const int dst_pass_offset = dst_params.get_pass_offset(pass_type, PassMode::DENOISED);
if (dst_pass_offset == PASS_UNUSED) {
continue;
}
const int src_pass_offset = src_params.get_pass_offset(pass_type, PassMode::DENOISED);
if (src_pass_offset == PASS_UNUSED) {
continue;
}
pass_offsets[num_passes].dst_offset = dst_pass_offset;
pass_offsets[num_passes].src_offset = src_pass_offset;
++num_passes;
}
/* Copy passes. */
/* TODO(sergey): Make it more reusable, allowing implement copy of noisy passes. */
const int64_t dst_width = dst_params.width;
const int64_t dst_height = dst_params.height;
const int64_t dst_pass_stride = dst_params.pass_stride;
const int64_t dst_num_pixels = dst_width * dst_height;
const int64_t src_pass_stride = src_params.pass_stride;
const int64_t src_offset_in_floats = src_offset * src_pass_stride;
const float *src_pixel = src->buffer.data() + src_offset_in_floats;
float *dst_pixel = dst->buffer.data();
for (int i = 0; i < dst_num_pixels;
++i, src_pixel += src_pass_stride, dst_pixel += dst_pass_stride)
{
for (int pass_offset_idx = 0; pass_offset_idx < num_passes; ++pass_offset_idx) {
const int dst_pass_offset = pass_offsets[pass_offset_idx].dst_offset;
const int src_pass_offset = pass_offsets[pass_offset_idx].src_offset;
/* TODO(sergey): Support non-RGBA passes. */
dst_pixel[dst_pass_offset + 0] = src_pixel[src_pass_offset + 0];
dst_pixel[dst_pass_offset + 1] = src_pixel[src_pass_offset + 1];
dst_pixel[dst_pass_offset + 2] = src_pixel[src_pass_offset + 2];
dst_pixel[dst_pass_offset + 3] = src_pixel[src_pass_offset + 3];
}
}
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "device/memory.h"
#include "graph/node.h"
#include "scene/pass.h"
#include "kernel/types.h"
#include "util/string.h"
#include "util/unique_ptr.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
class Device;
struct DeviceDrawParams;
struct float4;
/* NOTE: Is not a real scene node. Using Node API for ease of (de)serialization. */
class BufferPass : public Node {
public:
NODE_DECLARE
PassType type = PASS_NONE;
PassMode mode = PassMode::NOISY;
ustring name;
bool include_albedo = false;
ustring lightgroup;
int offset = -1;
BufferPass();
explicit BufferPass(const Pass *scene_pass);
BufferPass(BufferPass &&other) noexcept = default;
BufferPass(const BufferPass &other) = default;
BufferPass &operator=(BufferPass &&other) = default;
BufferPass &operator=(const BufferPass &other) = default;
~BufferPass() override = default;
PassInfo get_info() const;
bool operator==(const BufferPass &other) const
{
return type == other.type && mode == other.mode && name == other.name &&
include_albedo == other.include_albedo && lightgroup == other.lightgroup &&
offset == other.offset;
}
bool operator!=(const BufferPass &other) const
{
return !(*this == other);
}
};
/* Buffer Parameters
* Size of render buffer and how it fits in the full image (border render). */
/* NOTE: Is not a real scene node. Using Node API for ease of (de)serialization. */
class BufferParams : public Node {
public:
NODE_DECLARE
/* Width/height of the physical buffer. */
int width = 0;
int height = 0;
/* Windows defines which part of the buffers is visible. The part outside of the window is
* considered an `overscan`.
*
* Window X and Y are relative to the position of the buffer in the full buffer. */
int window_x = 0;
int window_y = 0;
int window_width = 0;
int window_height = 0;
/* Offset into and width/height of the full buffer. */
int full_x = 0;
int full_y = 0;
int full_width = 0;
int full_height = 0;
/* Runtime fields, only valid after `update_passes()` or `update_offset_stride()`. */
int offset = -1, stride = -1;
/* Runtime fields, only valid after `update_passes()`. */
int pass_stride = -1;
/* Properties which are used for accessing buffer pixels outside of scene graph. */
vector<BufferPass> passes;
ustring layer;
ustring view;
int samples = 0;
float exposure = 1.0f;
bool use_approximate_shadow_catcher = false;
bool use_transparent_background = false;
BufferParams();
BufferParams(BufferParams &&other) noexcept = default;
BufferParams(const BufferParams &other) = default;
BufferParams &operator=(BufferParams &&other) = default;
BufferParams &operator=(const BufferParams &other) = default;
~BufferParams() override = default;
/* Pre-calculate all fields which depends on the passes.
*
* When the scene passes are given, the buffer passes will be created from them and stored in
* this params, and then params are updated for those passes.
* The `update_passes()` without parameters updates offsets and strides which are stored outside
* of the passes. */
void update_passes();
void update_passes(const unique_ptr_vector<Pass> &scene_passes);
/* Returns PASS_UNUSED if there is no such pass in the buffer. */
int get_pass_offset(PassType type, PassMode mode = PassMode::NOISY) const;
/* Returns nullptr if pass with given name does not exist. */
const BufferPass *find_pass(string_view name) const;
const BufferPass *find_pass(PassType type, PassMode mode = PassMode::NOISY) const;
/* Get display pass from its name.
* Will do special logic to replace combined pass with shadow catcher matte. */
const BufferPass *get_actual_display_pass(PassType type, PassMode mode = PassMode::NOISY) const;
const BufferPass *get_actual_display_pass(const BufferPass *pass) const;
void update_offset_stride();
bool modified(const BufferParams &other) const;
protected:
void reset_pass_offset();
/* Multiplied by 2 to be able to store noisy and denoised pass types. */
static constexpr int kNumPassOffsets = PASS_NUM * 2;
/* Indexed by an index derived from pass type and mode, indicates offset of the corresponding
* pass in the buffer.
* If there are multiple passes with same type and mode contains lowest offset of all of them. */
int pass_offset_[kNumPassOffsets];
};
/* Render Buffers */
class RenderBuffers {
public:
/* buffer parameters */
BufferParams params;
/* float buffer */
device_vector<float> buffer;
explicit RenderBuffers(Device *device);
~RenderBuffers();
void reset(const BufferParams &params);
void zero();
bool copy_from_device();
void copy_to_device();
};
/* Copy denoised passes form source to destination.
*
* Buffer parameters are provided explicitly, allowing to copy pixels between render buffers which
* content corresponds to a render result at a non-unit resolution divider.
*
* `src_offset` allows to offset source pixel index which is used when a fraction of the source
* buffer is to be copied.
*
* Copy happens of the number of pixels in the destination. */
void render_buffers_host_copy_denoised(RenderBuffers *dst,
const BufferParams &dst_params,
const RenderBuffers *src,
const BufferParams &src_params,
const size_t src_offset = 0);
CCL_NAMESPACE_END

View File

@@ -0,0 +1,90 @@
/* SPDX-FileCopyrightText: 2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "session/cache_eviction.h"
#include <chrono>
#include "util/math_base.h"
#include "util/time.h"
CCL_NAMESPACE_BEGIN
CacheEvictionManager::CacheEvictionManager(bool background) : background_(background) {}
void CacheEvictionManager::reset()
{
render_tile_count_ = 0;
viewport_last_activity_ = 0.0;
viewport_was_navigating_ = false;
render_tile_count_ = 0;
}
void CacheEvictionManager::set_navigating(bool navigating)
{
navigating_ = navigating;
}
bool CacheEvictionManager::is_navigating() const
{
return (background_) ? false : navigating_;
}
bool CacheEvictionManager::need_eviction(bool idle, bool switched_to_new_tile)
{
/* Final render. */
if (background_) {
/* Evict before rendering the next tile, except the first one where we
* can't determine what was shared with other tiles. */
if (idle || !switched_to_new_tile) {
return false;
}
render_tile_count_++;
return render_tile_count_ >= 2;
}
/* Viewport render. */
const bool navigating = navigating_;
if (navigating) {
/* No eviction while navigating. */
viewport_last_activity_ = 0.0;
}
else if (viewport_was_navigating_) {
/* Start eviction timer when navigating stops. */
viewport_last_activity_ = time_dt();
}
viewport_was_navigating_ = navigating;
if (!idle) {
/* Restart eviction timer while not idle. */
viewport_last_activity_ = time_dt();
return false;
}
if (wait_time(idle) != std::chrono::milliseconds::zero()) {
/* Not ready to evict yet. */
return false;
}
/* Eviction needed now, clear existing timer. */
viewport_last_activity_ = 0.0;
return true;
}
std::chrono::milliseconds CacheEvictionManager::wait_time(bool idle) const
{
if (!idle || background_ || viewport_last_activity_ == 0.0 || viewport_was_navigating_) {
/* No eviction pending. */
return std::chrono::milliseconds::max();
}
/* Wait for VIEWPORT_EVICTION_DELAY after last activity. */
const double elapsed = time_dt() - viewport_last_activity_;
const double remaining = VIEWPORT_EVICTION_DELAY - elapsed;
return std::chrono::milliseconds(int64_t(max(0.0, remaining) * 1000.0));
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <chrono>
#include "util/types.h"
CCL_NAMESPACE_BEGIN
/* CacheEvictionManager has decides when texture cache eviction should
* happen in a render session. Different policies are used for viewport
* and final rendering. */
class CacheEvictionManager {
public:
explicit CacheEvictionManager(bool background);
/* Reset state when starting a new render. */
void reset();
/* Set and query if viewport navigation is happening. */
void set_navigating(bool navigating);
bool is_navigating() const;
/* For a render iteration, check if cache eviction is needed. */
bool need_eviction(bool idle, bool switched_to_new_tile);
/* Wait time until cache eviction needs to be performed. */
std::chrono::milliseconds wait_time(bool idle) const;
private:
const bool background_;
bool navigating_ = false;
double viewport_last_activity_ = 0.0;
bool viewport_was_navigating_ = false;
int render_tile_count_ = 0;
static constexpr double VIEWPORT_EVICTION_DELAY = 2.0;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,707 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "session/denoising.h"
#include "device/cpu/device.h"
#include "session/display_driver.h"
#include "util/map.h"
#include "util/task.h"
#include <OpenImageIO/filesystem.h>
CCL_NAMESPACE_BEGIN
/* Utility Functions */
/* Splits in at its last dot, setting suffix to the part after the dot and in to the part before
* it. Returns whether a dot was found. */
static bool split_last_dot(string &in, string &suffix)
{
const size_t pos = in.rfind(".");
if (pos == string::npos) {
return false;
}
suffix = in.substr(pos + 1);
in = in.substr(0, pos);
return true;
}
/* Separate channel names as generated by Blender.
* If views is true:
* Inputs are expected in the form RenderLayer.Pass.View.Channel, sets renderlayer to
* "RenderLayer.View" Otherwise: Inputs are expected in the form RenderLayer.Pass.Channel */
static bool parse_channel_name(
string name, string &renderlayer, string &pass, string &channel, bool multiview_channels)
{
if (!split_last_dot(name, channel)) {
return false;
}
string view;
if (multiview_channels && !split_last_dot(name, view)) {
return false;
}
if (!split_last_dot(name, pass)) {
return false;
}
renderlayer = name;
if (multiview_channels) {
renderlayer += "." + view;
}
return true;
}
/* Channel Mapping */
struct ChannelMapping {
int channel;
string name;
};
static void fill_mapping(vector<ChannelMapping> &map, int pos, string name, string channels)
{
for (const char *chan = channels.c_str(); *chan; chan++) {
map.push_back({pos++, name + "." + *chan});
}
}
static const int INPUT_NUM_CHANNELS = 13;
static const int INPUT_NOISY_IMAGE = 0;
static const int INPUT_DENOISING_NORMAL = 3;
static const int INPUT_DENOISING_ALBEDO = 6;
static const int INPUT_MOTION = 9;
static vector<ChannelMapping> input_channels()
{
vector<ChannelMapping> map;
fill_mapping(map, INPUT_NOISY_IMAGE, "Combined", "RGB");
fill_mapping(map, INPUT_DENOISING_NORMAL, "Denoising Normal", "XYZ");
fill_mapping(map, INPUT_DENOISING_ALBEDO, "Denoising Albedo", "RGB");
fill_mapping(map, INPUT_MOTION, "Vector", "XYZW");
return map;
}
static const int OUTPUT_NUM_CHANNELS = 3;
static vector<ChannelMapping> output_channels()
{
vector<ChannelMapping> map;
fill_mapping(map, 0, "Combined", "RGB");
return map;
}
/* Render-layer Handling. */
bool DenoiseImageLayer::detect_denoising_channels()
{
/* Map device input to image channels. */
input_to_image_channel.clear();
input_to_image_channel.resize(INPUT_NUM_CHANNELS, -1);
for (const ChannelMapping &mapping : input_channels()) {
const vector<string>::iterator i = find(channels.begin(), channels.end(), mapping.name);
if (i == channels.end()) {
return false;
}
const size_t input_channel = mapping.channel;
const size_t layer_channel = i - channels.begin();
input_to_image_channel[input_channel] = layer_to_image_channel[layer_channel];
}
/* Map device output to image channels. */
output_to_image_channel.clear();
output_to_image_channel.resize(OUTPUT_NUM_CHANNELS, -1);
for (const ChannelMapping &mapping : output_channels()) {
const vector<string>::iterator i = find(channels.begin(), channels.end(), mapping.name);
if (i == channels.end()) {
return false;
}
const size_t output_channel = mapping.channel;
const size_t layer_channel = i - channels.begin();
output_to_image_channel[output_channel] = layer_to_image_channel[layer_channel];
}
/* Check that all buffer channels are correctly set. */
for (int i = 0; i < INPUT_NUM_CHANNELS; i++) {
assert(input_to_image_channel[i] >= 0);
}
for (int i = 0; i < OUTPUT_NUM_CHANNELS; i++) {
assert(output_to_image_channel[i] >= 0);
}
return true;
}
bool DenoiseImageLayer::match_channels(const std::vector<string> &channelnames,
const std::vector<string> &neighbor_channelnames)
{
vector<int> &mapping = previous_output_to_image_channel;
assert(mapping.empty());
mapping.resize(output_to_image_channel.size(), -1);
for (int i = 0; i < output_to_image_channel.size(); i++) {
const string &channel = channelnames[output_to_image_channel[i]];
const std::vector<string>::const_iterator frame_channel = find(
neighbor_channelnames.begin(), neighbor_channelnames.end(), channel);
if (frame_channel == neighbor_channelnames.end()) {
return false;
}
mapping[i] = frame_channel - neighbor_channelnames.begin();
}
return true;
}
/* Denoise Task */
DenoiseTask::DenoiseTask(Device *device, DenoiserPipeline *denoiser, const int frame)
: denoiser(denoiser), device(device), frame(frame), current_layer(0), buffers(device)
{
}
DenoiseTask::~DenoiseTask()
{
free();
}
/* Denoiser Operations */
bool DenoiseTask::load_input_pixels(const int layer)
{
/* Load center image */
const DenoiseImageLayer &image_layer = image.layers[layer];
float *buffer_data = buffers.buffer.data();
image.read_pixels(image_layer, buffers.params, buffer_data);
/* Load previous image */
if (frame > 0 && !image.read_previous_pixels(image_layer, buffers.params, buffer_data)) {
error = "Failed to read neighbor frame pixels";
return false;
}
/* Copy to device */
buffers.buffer.copy_to_device();
return true;
}
/* Task stages */
static void add_pass(unique_ptr_vector<Pass> &passes,
PassType type,
PassMode mode = PassMode::NOISY)
{
unique_ptr<Pass> pass = make_unique<Pass>();
pass->set_type(type);
pass->set_mode(mode);
passes.push_back(std::move(pass));
}
bool DenoiseTask::load()
{
const string center_filepath = denoiser->input[frame];
if (!image.load(center_filepath, error)) {
return false;
}
/* Use previous frame output as input for subsequent frames. */
if (frame > 0 && !image.load_previous(denoiser->output[frame - 1], error)) {
return false;
}
if (image.layers.empty()) {
error = "No image layers found to denoise in " + center_filepath;
return false;
}
/* Enable temporal denoising for frames after the first (which will use the output from the
* previous frames). */
DenoiseParams params = denoiser->denoiser->get_params();
params.temporally_stable = frame > 0;
denoiser->denoiser->set_params(params);
/* Allocate device buffer. */
unique_ptr_vector<Pass> passes;
add_pass(passes, PassType::PASS_COMBINED);
add_pass(passes, PassType::PASS_DENOISING_ALBEDO);
add_pass(passes, PassType::PASS_DENOISING_NORMAL);
add_pass(passes, PassType::PASS_MOTION);
add_pass(passes, PassType::PASS_DENOISING_PREVIOUS);
add_pass(passes, PassType::PASS_COMBINED, PassMode::DENOISED);
BufferParams buffer_params;
buffer_params.width = image.width;
buffer_params.height = image.height;
buffer_params.full_x = 0;
buffer_params.full_y = 0;
buffer_params.full_width = image.width;
buffer_params.full_height = image.height;
buffer_params.update_passes(passes);
passes.clear();
buffers.reset(buffer_params);
/* Read pixels for first layer. */
current_layer = 0;
if (!load_input_pixels(current_layer)) {
return false;
}
return true;
}
bool DenoiseTask::exec()
{
for (current_layer = 0; current_layer < image.layers.size(); current_layer++) {
/* Read pixels for secondary layers, first was already loaded. */
if (current_layer > 0) {
if (!load_input_pixels(current_layer)) {
return false;
}
}
/* Run task on device. */
denoiser->denoiser->denoise_buffer(buffers.params, buffers.params, &buffers, 1, true);
/* Copy denoised pixels from device. */
buffers.buffer.copy_from_device();
float *result = buffers.buffer.data();
float *out = image.pixels.data();
const DenoiseImageLayer &layer = image.layers[current_layer];
const int *output_to_image_channel = layer.output_to_image_channel.data();
for (int y = 0; y < image.height; y++) {
for (int x = 0; x < image.width; x++, result += buffers.params.pass_stride) {
for (int j = 0; j < OUTPUT_NUM_CHANNELS; j++) {
const int offset = buffers.params.get_pass_offset(PASS_COMBINED, PassMode::DENOISED);
const int image_channel = output_to_image_channel[j];
out[image.num_channels * x + image_channel] = result[offset + j];
}
}
out += image.num_channels * image.width;
}
}
return true;
}
bool DenoiseTask::save()
{
const bool ok = image.save_output(denoiser->output[frame], error);
free();
return ok;
}
void DenoiseTask::free()
{
image.free();
buffers.buffer.free();
}
/* Denoise Image Storage */
DenoiseImage::DenoiseImage()
{
width = 0;
height = 0;
num_channels = 0;
samples = 0;
}
DenoiseImage::~DenoiseImage()
{
free();
}
void DenoiseImage::close_input()
{
in_previous.reset();
}
void DenoiseImage::free()
{
close_input();
pixels.clear();
}
bool DenoiseImage::parse_channels(const ImageSpec &in_spec, string &error)
{
const std::vector<string> &channels = in_spec.channelnames;
const ParamValue *multiview = in_spec.find_attribute("multiView");
const bool multiview_channels = (multiview && multiview->type().basetype == TypeDesc::STRING &&
multiview->type().arraylen >= 2);
layers.clear();
/* Loop over all the channels in the file, parse their name and sort them
* by RenderLayer.
* Channels that can't be parsed are directly passed through to the output. */
map<string, DenoiseImageLayer> file_layers;
for (int i = 0; i < channels.size(); i++) {
string layer;
string pass;
string channel;
if (parse_channel_name(channels[i], layer, pass, channel, multiview_channels)) {
file_layers[layer].channels.push_back(pass + "." + channel);
file_layers[layer].layer_to_image_channel.push_back(i);
}
}
/* Loop over all detected RenderLayers, check whether they contain a full set of input channels.
* Any channels that won't be processed internally are also passed through. */
for (map<string, DenoiseImageLayer>::iterator i = file_layers.begin(); i != file_layers.end();
++i)
{
const string &name = i->first;
DenoiseImageLayer &layer = i->second;
/* Check for full pass set. */
if (!layer.detect_denoising_channels()) {
continue;
}
layer.name = name;
layer.samples = samples;
/* If the sample value isn't set yet, check if there is a layer-specific one in the input file.
*/
if (layer.samples < 1) {
const string sample_string = in_spec.get_string_attribute("cycles." + name + ".samples", "");
if (!sample_string.empty()) {
if (!sscanf(sample_string.c_str(), "%d", &layer.samples)) {
error = "Failed to parse samples metadata: " + sample_string;
return false;
}
}
}
if (layer.samples < 1) {
error = string_printf(
"No sample number specified in the file for layer %s or on the command line",
name.c_str());
return false;
}
layers.push_back(layer);
}
return true;
}
void DenoiseImage::read_pixels(const DenoiseImageLayer &layer,
const BufferParams &params,
float *input_pixels)
{
/* Pixels from center file have already been loaded into pixels.
* We copy a subset into the device input buffer with channels reshuffled. */
const int *input_to_image_channel = layer.input_to_image_channel.data();
for (int i = 0; i < width * height; i++) {
for (int j = 0; j < 3; ++j) {
const int offset = params.get_pass_offset(PASS_COMBINED);
const int image_channel = input_to_image_channel[INPUT_NOISY_IMAGE + j];
input_pixels[i * params.pass_stride + offset + j] =
pixels[((size_t)i) * num_channels + image_channel];
}
for (int j = 0; j < 3; ++j) {
const int offset = params.get_pass_offset(PASS_DENOISING_NORMAL);
const int image_channel = input_to_image_channel[INPUT_DENOISING_NORMAL + j];
input_pixels[i * params.pass_stride + offset + j] =
pixels[((size_t)i) * num_channels + image_channel];
}
for (int j = 0; j < 3; ++j) {
const int offset = params.get_pass_offset(PASS_DENOISING_ALBEDO);
const int image_channel = input_to_image_channel[INPUT_DENOISING_ALBEDO + j];
input_pixels[i * params.pass_stride + offset + j] =
pixels[((size_t)i) * num_channels + image_channel];
}
for (int j = 0; j < 4; ++j) {
const int offset = params.get_pass_offset(PASS_MOTION);
const int image_channel = input_to_image_channel[INPUT_MOTION + j];
input_pixels[i * params.pass_stride + offset + j] =
pixels[((size_t)i) * num_channels + image_channel];
}
}
}
bool DenoiseImage::read_previous_pixels(const DenoiseImageLayer &layer,
const BufferParams &params,
float *input_pixels)
{
/* Load pixels from neighboring frames, and copy them into device buffer
* with channels reshuffled. */
const size_t num_pixels = (size_t)width * (size_t)height;
const int num_channels = in_previous->spec().nchannels;
array<float> neighbor_pixels(num_pixels * num_channels);
if (!in_previous->read_image(0, 0, 0, num_channels, TypeDesc::FLOAT, neighbor_pixels.data())) {
return false;
}
const int *output_to_image_channel = layer.previous_output_to_image_channel.data();
for (int i = 0; i < width * height; i++) {
for (int j = 0; j < 3; ++j) {
const int offset = params.get_pass_offset(PASS_DENOISING_PREVIOUS);
const int image_channel = output_to_image_channel[j];
input_pixels[i * params.pass_stride + offset + j] =
neighbor_pixels[((size_t)i) * num_channels + image_channel];
}
}
return true;
}
bool DenoiseImage::read_pixels(ImageInput *in)
{
/* For multi-part EXR each subimage contains a pass, so we'll read them all. */
const int num_subimages = in_spec.get_int_attribute("oiio:subimages", 1);
vector<string> channelnames;
vector<int> num_channels_subimage;
for (int s = 0; s < num_subimages; s++) {
const ImageSpec spec = in->spec(s);
num_channels_subimage.push_back(spec.nchannels);
for (const string &name : spec.channelnames) {
channelnames.push_back(name);
}
}
num_channels = int(channelnames.size());
pixels.resize(size_t(width) * size_t(height) * num_channels);
/* Read all channels from each subimage. */
const int64_t xstride = int64_t(num_channels) * sizeof(float);
size_t channel_offset = 0;
for (size_t s = 0; s < num_channels_subimage.size(); s++) {
if (!in->read_image(s,
0,
0,
num_channels_subimage[s],
TypeDesc::FLOAT,
pixels.data() + channel_offset,
xstride))
{
return false;
}
channel_offset += num_channels_subimage[s];
}
/* Update in_spec to reflect the flattened channel list for use in parse_channels. */
in->seek_subimage(0, 0);
in_spec.channelnames = channelnames;
in_spec.nchannels = num_channels;
in_spec.channelformats.clear();
in_spec.format = TypeDesc::FLOAT;
return true;
}
bool DenoiseImage::load(const string &in_filepath, string &error)
{
if (!Filesystem::is_regular(in_filepath)) {
error = "Couldn't find file: " + in_filepath;
return false;
}
unique_ptr<ImageInput> in(ImageInput::open(in_filepath));
if (!in) {
error = "Couldn't open file: " + in_filepath;
return false;
}
in_spec = in->spec();
width = in_spec.width;
height = in_spec.height;
if (!read_pixels(in.get())) {
error = "Failed to read image: " + in_filepath;
return false;
}
if (!parse_channels(in_spec, error)) {
return false;
}
if (layers.empty()) {
error = "Could not find a render layer containing denoising data and motion vector passes";
return false;
}
return true;
}
bool DenoiseImage::load_previous(const string &filepath, string &error)
{
if (!Filesystem::is_regular(filepath)) {
error = "Couldn't find neighbor frame: " + filepath;
return false;
}
unique_ptr<ImageInput> in_neighbor(ImageInput::open(filepath));
if (!in_neighbor) {
error = "Couldn't open neighbor frame: " + filepath;
return false;
}
const ImageSpec &neighbor_spec = in_neighbor->spec();
if (neighbor_spec.width != width || neighbor_spec.height != height) {
error = "Neighbor frame has different dimensions: " + filepath;
return false;
}
for (DenoiseImageLayer &layer : layers) {
if (!layer.match_channels(in_spec.channelnames, neighbor_spec.channelnames)) {
error = "Neighbor frame misses denoising data passes: " + filepath;
return false;
}
}
in_previous = std::move(in_neighbor);
return true;
}
bool DenoiseImage::save_output(const string &out_filepath, string &error)
{
/* Save image with identical dimensions, channels and metadata. */
ImageSpec out_spec = in_spec;
/* Ensure that the output frame contains sample information even if the input didn't. */
for (int i = 0; i < layers.size(); i++) {
const string name = "cycles." + layers[i].name + ".samples";
if (!out_spec.find_attribute(name, TypeDesc::STRING)) {
out_spec.attribute(name, TypeDesc::STRING, string_printf("%d", layers[i].samples));
}
}
/* We don't need input anymore at this point, and will possibly
* overwrite the same file. */
close_input();
/* Write to temporary file path, so we denoise images in place and don't
* risk destroying files when something goes wrong in file saving. */
const string extension = OIIO::Filesystem::extension(out_filepath);
const string unique_name = ".denoise-tmp-" + OIIO::Filesystem::unique_path();
const string tmp_filepath = out_filepath + unique_name + extension;
unique_ptr<ImageOutput> out(ImageOutput::create(tmp_filepath));
if (!out) {
error = "Failed to open temporary file " + tmp_filepath + " for writing";
return false;
}
/* Open temporary file and write image buffers. */
if (!out->open(tmp_filepath, out_spec)) {
error = "Failed to open file " + tmp_filepath + " for writing: " + out->geterror();
return false;
}
bool ok = true;
if (!out->write_image(TypeDesc::FLOAT, pixels.data())) {
error = "Failed to write to file " + tmp_filepath + ": " + out->geterror();
ok = false;
}
if (!out->close()) {
error = "Failed to save to file " + tmp_filepath + ": " + out->geterror();
ok = false;
}
out.reset();
/* Copy temporary file to output filepath. */
string rename_error;
if (ok && !OIIO::Filesystem::rename(tmp_filepath, out_filepath, rename_error)) {
error = "Failed to move denoised image to " + out_filepath + ": " + rename_error;
ok = false;
}
if (!ok) {
OIIO::Filesystem::remove(tmp_filepath);
}
return ok;
}
/* File pattern handling and outer loop over frames */
DenoiserPipeline::DenoiserPipeline(DeviceInfo &denoiser_device_info, const DenoiseParams &params)
{
/* Initialize task scheduler. */
TaskScheduler::init();
/* Initialize device. */
device = Device::create(denoiser_device_info, stats, profiler, true);
device->load_kernels(KERNEL_FEATURE_DENOISING);
vector<DeviceInfo> cpu_devices;
device_cpu_info(cpu_devices);
cpu_device = device_cpu_create(cpu_devices[0], device->stats, device->profiler, true);
denoiser = Denoiser::create(device.get(), cpu_device.get(), params, GraphicsInteropDevice());
if (denoiser) {
denoiser->load_kernels(nullptr);
}
}
DenoiserPipeline::~DenoiserPipeline()
{
denoiser.reset();
device.reset();
TaskScheduler::exit();
}
bool DenoiserPipeline::run()
{
assert(input.size() == output.size());
const int num_frames = output.size();
if (!denoiser) {
error = "Failed to create denoiser";
return false;
}
for (int frame = 0; frame < num_frames; frame++) {
/* Skip empty output paths. */
if (output[frame].empty()) {
continue;
}
/* Execute task. */
DenoiseTask task(device.get(), this, frame);
if (!task.load()) {
error = task.error;
return false;
}
if (!task.exec()) {
error = task.error;
return false;
}
if (!task.save()) {
error = task.error;
return false;
}
task.free();
}
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,177 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
/* TODO(sergey): Make it explicit and clear when something is a denoiser, its pipeline or
* parameters. Currently it is an annoying mixture of terms used interchangeably. */
#include "device/device.h"
#include "integrator/denoiser.h"
#include "session/buffers.h"
#include "util/string.h"
#include "util/unique_ptr.h"
#include "util/vector.h"
#include <OpenImageIO/imageio.h>
OIIO_NAMESPACE_USING
CCL_NAMESPACE_BEGIN
/* Denoiser pipeline */
class DenoiserPipeline {
public:
DenoiserPipeline(DeviceInfo &denoiser_device_info, const DenoiseParams &params);
~DenoiserPipeline();
bool run();
/* Error message after running, in case of failure. */
string error;
/* Sequential list of frame filepaths to denoise. */
vector<string> input;
/* Sequential list of frame filepaths to write result to. Empty entries
* are skipped, so only a subset of the sequence can be denoised while
* taking into account all input frames. */
vector<string> output;
protected:
friend class DenoiseTask;
Stats stats;
Profiler profiler;
unique_ptr<Device> device;
unique_ptr<Device> cpu_device;
std::unique_ptr<Denoiser> denoiser;
};
/* Denoise Image Layer */
struct DenoiseImageLayer {
string name;
/* All channels belonging to this DenoiseImageLayer. */
vector<string> channels;
/* Layer to image channel mapping. */
vector<int> layer_to_image_channel;
/* Sample amount that was used for rendering this layer. */
int samples;
/* Device input channel will be copied from image channel input_to_image_channel[i]. */
vector<int> input_to_image_channel;
/* Write i-th channel of the processing output to output_to_image_channel[i]-th channel of the
* file. */
vector<int> output_to_image_channel;
/* output_to_image_channel of the previous frame, if used. */
vector<int> previous_output_to_image_channel;
/* Detect whether this layer contains a full set of channels and set up the offsets accordingly.
*/
bool detect_denoising_channels();
/* Map the channels of a secondary frame to the channels that are required for processing,
* fill neighbor_input_to_image_channel if all are present or return false if a channel are
* missing. */
bool match_channels(const std::vector<string> &channelnames,
const std::vector<string> &neighbor_channelnames);
};
/* Denoise Image Data */
class DenoiseImage {
public:
DenoiseImage();
~DenoiseImage();
/* Dimensions */
int width, height, num_channels;
/* Samples */
int samples;
/* Pixel buffer with interleaved channels. */
array<float> pixels;
/* Image file handles */
ImageSpec in_spec;
unique_ptr<ImageInput> in_previous;
/* Render layers */
vector<DenoiseImageLayer> layers;
void free();
/* Open the input image, parse its channels, open the output image and allocate the output
* buffer. */
bool load(const string &in_filepath, string &error);
/* Load neighboring frames. */
bool load_previous(const string &in_filepath, string &error);
/* Load subset of pixels from file buffer into input buffer, as needed for denoising
* on the device. Channels are reshuffled following the provided mapping. */
void read_pixels(const DenoiseImageLayer &layer,
const BufferParams &params,
float *input_pixels);
bool read_previous_pixels(const DenoiseImageLayer &layer,
const BufferParams &params,
float *input_pixels);
bool save_output(const string &out_filepath, string &error);
protected:
/* Parse input file channels, separate them into DenoiseImageLayers,
* detect DenoiseImageLayers with full channel sets,
* fill layers and set up the output channels and passthrough map. */
bool parse_channels(const ImageSpec &in_spec, string &error);
/* Read pixels from an open ImageInput into the pixels buffer.
* Updates in_spec, num_channels, and pixels. */
bool read_pixels(ImageInput *in);
void close_input();
};
/* Denoise Task */
class DenoiseTask {
public:
DenoiseTask(Device *device, DenoiserPipeline *denoiser, const int frame);
~DenoiseTask();
/* Task stages */
bool load();
bool exec();
bool save();
void free();
string error;
protected:
/* Denoiser parameters and device */
DenoiserPipeline *denoiser;
Device *device;
/* Frame number to be denoised */
int frame;
/* Image file data */
DenoiseImage image;
int current_layer;
RenderBuffers buffers;
/* Task handling */
bool load_input_pixels(const int layer);
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,86 @@
/* SPDX-FileCopyrightText: 2021-2025 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "session/display_driver.h"
#ifdef _WIN32
# include "util/windows.h"
#else
# include <unistd.h>
#endif
CCL_NAMESPACE_BEGIN
GraphicsInteropBuffer::~GraphicsInteropBuffer()
{
clear();
}
void GraphicsInteropBuffer::assign(GraphicsInteropDevice::Type type, int64_t handle, size_t size)
{
clear();
type_ = type;
handle_ = handle;
own_handle_ = true;
size_ = size;
}
bool GraphicsInteropBuffer::is_empty() const
{
return handle_ == 0;
}
void GraphicsInteropBuffer::zero()
{
need_zero_ = true;
}
void GraphicsInteropBuffer::clear()
{
if (type_ == GraphicsInteropDevice::VULKAN && handle_ && own_handle_) {
#ifdef _WIN32
CloseHandle(HANDLE(handle_));
#else
close(handle_);
#endif
}
type_ = GraphicsInteropDevice::NONE;
handle_ = 0;
size_ = 0;
need_zero_ = false;
own_handle_ = false;
}
GraphicsInteropDevice::Type GraphicsInteropBuffer::get_type() const
{
return type_;
}
size_t GraphicsInteropBuffer::get_size() const
{
return size_;
}
bool GraphicsInteropBuffer::has_new_handle() const
{
return own_handle_;
}
bool GraphicsInteropBuffer::take_zero()
{
bool need_zero = need_zero_;
need_zero_ = false;
return need_zero;
}
int64_t GraphicsInteropBuffer::take_handle()
{
assert(own_handle_);
own_handle_ = false;
return handle_;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,194 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "util/half.h"
#include "util/math_int2.h"
#include "util/types.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
/* Info about the display device that will be used for graphics interop, so it
* can be verified if interop is compatible with the rendering device. */
class GraphicsInteropDevice {
public:
enum Type {
NONE,
OPENGL,
VULKAN,
METAL,
};
Type type = NONE;
vector<uint8_t> uuid;
};
/* Handle to a native graphics API pixel buffer. If supported, the rendering device
* may write directly to this buffer instead of calling map_texture_buffer() and
* unmap_texture_buffer().
*
* This must be a pixel buffer with the specified with and height, and half float
* with RGBA channels. */
class GraphicsInteropBuffer {
public:
GraphicsInteropBuffer() = default;
~GraphicsInteropBuffer();
GraphicsInteropBuffer(const GraphicsInteropBuffer &other) = delete;
GraphicsInteropBuffer &operator=(const GraphicsInteropBuffer &other) = delete;
GraphicsInteropBuffer(GraphicsInteropBuffer &&other) = delete;
GraphicsInteropBuffer &operator=(GraphicsInteropBuffer &&other) = delete;
/* Display Driver API. */
/* Assign handle. For Vulkan, this transfers ownership of the handle. */
void assign(GraphicsInteropDevice::Type type, int64_t handle, size_t size);
/* Is a handle assigned? */
bool is_empty() const;
/* Zero memory. */
void zero();
/* Clear handle. */
void clear();
/* Device graphics interop API. */
/* Get type of handle. */
GraphicsInteropDevice::Type get_type() const;
/* Get size of buffer. */
size_t get_size() const;
/* Is there a new handle to take ownership of? */
bool has_new_handle() const;
/* Take ownership of the handle. */
int64_t take_handle();
/* Take ownership of zeroing the buffer. */
bool take_zero();
protected:
/* The handle is expected to be:
* - OpenGL: pixel buffer object ID.
* - Vulkan on Windows: opaque handle for VkBuffer.
* - Vulkan on Unix: opaque file descriptor for VkBuffer.
* - Metal: MTLBuffer with unified memory. */
GraphicsInteropDevice::Type type_ = GraphicsInteropDevice::NONE;
int64_t handle_ = 0;
bool own_handle_ = false;
/* Actual size of the memory, which must be `>= width * height * sizeof(half4)`. */
size_t size_ = 0;
/* Clear the entire buffer before doing partial write to it. */
bool need_zero_ = false;
};
/* Display driver for efficient interactive display of renders.
*
* Host applications implement this interface for viewport rendering. For best performance, we
* recommend:
* - Allocating a texture on the GPU to be interactively updated
* - Using the graphics interop mechanism to avoid CPU-GPU copying overhead
* - Using a dedicated or thread-safe graphics API context for updates, to avoid
* blocking the host application.
*/
class DisplayDriver {
public:
DisplayDriver() = default;
virtual ~DisplayDriver() = default;
/* Render buffer parameters. */
struct Params {
public:
/* Render resolution, ignoring progressive resolution changes.
* The texture buffer should be allocated with this size. */
int2 size = make_int2(0, 0);
/* For border rendering, the full resolution of the render, and the offset within that larger
* render. */
int2 full_size = make_int2(0, 0);
int2 full_offset = make_int2(0, 0);
bool modified(const Params &other) const
{
return !(full_offset == other.full_offset && full_size == other.full_size &&
size == other.size);
}
};
virtual void next_tile_begin() = 0;
/* Update the render from the rendering thread.
*
* Cycles periodically updates the render to be displayed. For multithreaded updates with
* potentially multiple rendering devices, it will call these methods as follows.
*
* if (driver.update_begin(params, width, height)) {
* parallel_for_each(rendering_device) {
* buffer = driver.map_texture_buffer();
* if (buffer) {
* fill(buffer);
* driver.unmap_texture_buffer();
* }
* }
* driver.update_end();
* }
*
* The parameters may dynamically change due to camera changes in the scene, and resources should
* be re-allocated accordingly.
*
* The width and height passed to update_begin() are the effective render resolution taking into
* account progressive resolution changes, which may be equal to or smaller than the params.size.
* For efficiency, changes in this resolution should be handled without re-allocating resources,
* but rather by using a subset of the full resolution buffer. */
virtual bool update_begin(const Params &params, const int width, const int height) = 0;
virtual void update_end() = 0;
/* Optionally flush outstanding display commands before ending the render loop. */
virtual void flush() {};
virtual half4 *map_texture_buffer() = 0;
virtual void unmap_texture_buffer() = 0;
GraphicsInteropBuffer graphics_interop_buffer_;
/* Graphics interop to avoid CPU - GPU transfer. See GraphicsInteropBuffer for details. */
virtual GraphicsInteropDevice graphics_interop_get_device()
{
return GraphicsInteropDevice();
}
virtual void graphics_interop_update_buffer() {}
GraphicsInteropBuffer &graphics_interop_get_buffer()
{
return graphics_interop_buffer_;
}
/* (De)activate graphics context required for editing or deleting the graphics interop
* object.
*
* For example, destruction of the CUDA object associated with an OpenGL requires the
* OpenGL context to be active. */
virtual void graphics_interop_activate() {};
virtual void graphics_interop_deactivate() {};
/* Clear the display buffer by filling it with zeros. */
virtual void zero() = 0;
/* Draw the render using the native graphics API.
*
* Note that this may be called in parallel to updates. The implementation is responsible for
* mutex locking or other mechanisms to avoid conflicts.
*
* The parameters may have changed since the last update. The implementation is responsible for
* deciding to skip or adjust render display for such changes.
*
* Host application drawing the render buffer should use Session.draw(), which will
* call this method. */
virtual void draw(const Params &params) = 0;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,611 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "session/merge.h"
#include "util/array.h"
#include "util/map.h"
#include "util/time.h"
#include "util/unique_ptr.h"
#include <OpenImageIO/filesystem.h>
#include <OpenImageIO/imageio.h>
OIIO_NAMESPACE_USING
CCL_NAMESPACE_BEGIN
/* Merge Image Layer */
enum MergeChannelOp {
MERGE_CHANNEL_NOP,
MERGE_CHANNEL_COPY,
MERGE_CHANNEL_SUM,
MERGE_CHANNEL_AVERAGE,
MERGE_CHANNEL_SAMPLES,
};
struct MergeImagePass {
/* Full channel name. */
string channel_name;
/* Pass name. */
string name;
/* Channel format in the file. */
TypeDesc format;
/* Type of operation to perform when merging. */
MergeChannelOp op;
/* Offset of layer channels in input image. */
int offset;
/* Offset of layer channels in merged image. */
int merge_offset;
};
struct SampleCount {
/* Total number of samples. */
int total;
/* Buffer for actual number of samples rendered per pixel. */
array<float> per_pixel;
};
struct MergeImageLayer {
/* Layer name. */
string name;
/* Passes. */
vector<MergeImagePass> passes;
/* Sample amount that was used for rendering this layer. */
int samples;
/* Indicates if this layer has "Debug Sample Count" pass. */
bool has_sample_pass;
/* Offset of the "Debug Sample Count" pass if it exists. */
int sample_pass_offset;
};
/* Merge Image */
struct MergeImage {
/* OIIO file handle. */
unique_ptr<ImageInput> in;
/* Image file path. */
string filepath;
/* Render layers. */
vector<MergeImageLayer> layers;
};
/* Channel Parsing */
static MergeChannelOp parse_channel_operation(const string &pass_name)
{
if (pass_name == "Depth" || pass_name == "IndexMA" || pass_name == "IndexOB" ||
string_startswith(pass_name, "Crypto"))
{
return MERGE_CHANNEL_COPY;
}
if (string_startswith(pass_name, "Debug BVH") || string_startswith(pass_name, "Debug Ray") ||
string_startswith(pass_name, "Debug Render Time"))
{
return MERGE_CHANNEL_SUM;
}
if (string_startswith(pass_name, "Debug Sample Count")) {
return MERGE_CHANNEL_SAMPLES;
}
return MERGE_CHANNEL_AVERAGE;
}
/* Splits in at its last dot, setting suffix to the part after the dot and
* into the part before it. Returns whether a dot was found. */
static bool split_last_dot(string &in, string &suffix)
{
const size_t pos = in.rfind(".");
if (pos == string::npos) {
return false;
}
suffix = in.substr(pos + 1);
in = in.substr(0, pos);
return true;
}
/* Separate channel names as generated by Blender.
* Multiview format: RenderLayer.Pass.View.Channel
* Otherwise: RenderLayer.Pass.Channel */
static bool parse_channel_name(
string name, string &renderlayer, string &pass, string &channel, bool multiview_channels)
{
if (!split_last_dot(name, channel)) {
return false;
}
string view;
if (multiview_channels && !split_last_dot(name, view)) {
return false;
}
if (!split_last_dot(name, pass)) {
return false;
}
renderlayer = name;
if (multiview_channels) {
renderlayer += "." + view;
}
return true;
}
static bool parse_channels(const ImageSpec &in_spec,
vector<MergeImageLayer> &layers,
string &error)
{
const ParamValue *multiview = in_spec.find_attribute("multiView");
const bool multiview_channels = (multiview && multiview->type().basetype == TypeDesc::STRING &&
multiview->type().arraylen >= 2);
layers.clear();
/* Loop over all the channels in the file, parse their name and sort them
* by RenderLayer.
* Channels that can't be parsed are directly passed through to the output. */
map<string, MergeImageLayer> file_layers;
for (int i = 0; i < in_spec.nchannels; i++) {
MergeImagePass pass;
pass.channel_name = in_spec.channelnames[i];
pass.format = (!in_spec.channelformats.empty()) ? in_spec.channelformats[i] : in_spec.format;
pass.offset = i;
pass.merge_offset = i;
string layername;
string channelname;
if (parse_channel_name(
pass.channel_name, layername, pass.name, channelname, multiview_channels))
{
/* Channel part of a render layer. */
pass.op = parse_channel_operation(pass.name);
}
else {
/* Other channels are added in unnamed layer. */
layername = "";
pass.op = parse_channel_operation(pass.channel_name);
}
file_layers[layername].passes.push_back(pass);
}
/* If file contains a single unnamed layer, name it after the first layer metadata we find. */
if (file_layers.size() == 1 && file_layers.contains("")) {
for (const ParamValue &attrib : in_spec.extra_attribs) {
const string attrib_name = attrib.name().string();
if (string_startswith(attrib_name, "cycles.") && string_endswith(attrib_name, ".samples")) {
/* Extract layer name. */
const size_t start = strlen("cycles.");
const size_t end = attrib_name.size() - strlen(".samples");
const string layername = attrib_name.substr(start, end - start);
/* Reinsert as named instead of unnamed layer. */
const MergeImageLayer layer = file_layers[""];
file_layers.clear();
file_layers[layername] = layer;
}
}
}
/* Loop over all detected render-layers, check whether they contain a full set of input
* channels. Any channels that won't be processed internally are also passed through. */
for (auto &[name, layer] : file_layers) {
layer.name = name;
layer.samples = 0;
/* Determine number of samples from metadata. */
if (layer.name.empty()) {
layer.samples = 1;
}
else if (layer.samples < 1) {
const string sample_string = in_spec.get_string_attribute("cycles." + name + ".samples", "");
if (!sample_string.empty()) {
if (!sscanf(sample_string.c_str(), "%d", &layer.samples)) {
error = "Failed to parse samples metadata: " + sample_string;
return false;
}
}
}
if (layer.samples < 1) {
error = string_printf(
"No sample number specified in the file for layer %s or on the command line",
name.c_str());
return false;
}
/* Check if the layer has "Debug Sample Count" pass. */
auto sample_pass_it = find_if(
layer.passes.begin(), layer.passes.end(), [](const MergeImagePass &pass) {
return pass.name == "Debug Sample Count";
});
if (sample_pass_it != layer.passes.end()) {
layer.has_sample_pass = true;
layer.sample_pass_offset = distance(layer.passes.begin(), sample_pass_it);
}
else {
layer.has_sample_pass = false;
}
layers.push_back(layer);
}
return true;
}
static bool open_images(const vector<string> &filepaths, vector<MergeImage> &images, string &error)
{
for (const string &filepath : filepaths) {
unique_ptr<ImageInput> in(ImageInput::open(filepath));
if (!in) {
error = "Couldn't open file: " + filepath;
return false;
}
MergeImage image;
image.in = std::move(in);
image.filepath = filepath;
if (!parse_channels(image.in->spec(), image.layers, error)) {
return false;
}
if (image.layers.empty()) {
error = "Could not find a render layer for merging";
return false;
}
if (image.in->spec().deep) {
error = "Merging deep images not supported.";
return false;
}
if (!images.empty()) {
const ImageSpec &base_spec = images[0].in->spec();
const ImageSpec &spec = image.in->spec();
if (base_spec.width != spec.width || base_spec.height != spec.height ||
base_spec.depth != spec.depth || base_spec.format != spec.format ||
base_spec.deep != spec.deep)
{
error = "Images do not have matching size and data layout.";
return false;
}
}
images.push_back(std::move(image));
}
return true;
}
static void merge_render_time(ImageSpec &spec,
const vector<MergeImage> &images,
const string &name,
const bool average)
{
double time = 0.0;
for (const MergeImage &image : images) {
const string time_str = image.in->spec().get_string_attribute(name, "");
time += time_human_readable_to_seconds(time_str);
}
if (average) {
time /= images.size();
}
spec.attribute(name, TypeDesc::STRING, time_human_readable_from_seconds(time));
}
static void merge_layer_render_time(ImageSpec &spec,
const vector<MergeImage> &images,
const string &layer_name,
const string &time_name,
const bool average)
{
const string name = "cycles." + layer_name + "." + time_name;
double time = 0.0;
for (const MergeImage &image : images) {
const string time_str = image.in->spec().get_string_attribute(name, "");
time += time_human_readable_to_seconds(time_str);
}
if (average) {
time /= images.size();
}
spec.attribute(name, TypeDesc::STRING, time_human_readable_from_seconds(time));
}
static void merge_channels_metadata(vector<MergeImage> &images, ImageSpec &out_spec)
{
/* Based on first image. */
out_spec = images[0].in->spec();
/* Merge channels and compute offsets. */
out_spec.nchannels = 0;
out_spec.channelformats.clear();
out_spec.channelnames.clear();
for (MergeImage &image : images) {
for (MergeImageLayer &layer : image.layers) {
for (MergeImagePass &pass : layer.passes) {
/* Test if matching channel already exists in merged image. */
auto channel = find_if(
out_spec.channelnames.begin(),
out_spec.channelnames.end(),
[&pass](const auto &channel_name) { return pass.channel_name == channel_name; });
if (channel != out_spec.channelnames.end()) {
const int index = distance(out_spec.channelnames.begin(), channel);
pass.merge_offset = index;
/* First image wins for channels that can't be averaged or summed. */
if (pass.op == MERGE_CHANNEL_COPY) {
pass.op = MERGE_CHANNEL_NOP;
}
}
else {
/* Add new channel. */
pass.merge_offset = out_spec.nchannels;
out_spec.channelnames.push_back(pass.channel_name);
out_spec.channelformats.push_back(pass.format);
out_spec.nchannels++;
}
}
}
}
/* Merge metadata. */
merge_render_time(out_spec, images, "RenderTime", false);
map<string, int> layer_num_samples;
for (const MergeImage &image : images) {
for (const MergeImageLayer &layer : image.layers) {
if (!layer.name.empty()) {
layer_num_samples[layer.name] += layer.samples;
}
}
}
for (const auto &[layer_name, layer_samples] : layer_num_samples) {
const string name = "cycles." + layer_name + ".samples";
out_spec.attribute(name, TypeDesc::STRING, to_string(layer_samples));
merge_layer_render_time(out_spec, images, layer_name, "total_time", false);
merge_layer_render_time(out_spec, images, layer_name, "render_time", false);
merge_layer_render_time(out_spec, images, layer_name, "synchronization_time", true);
}
}
static void alloc_pixels(const ImageSpec &spec, array<float> &pixels)
{
const size_t width = spec.width;
const size_t height = spec.height;
const size_t num_channels = spec.nchannels;
const size_t num_pixels = width * height;
pixels.resize(num_pixels * num_channels);
}
static bool merge_pixels(const vector<MergeImage> &images,
const ImageSpec &out_spec,
const unordered_map<string, SampleCount> &layer_samples,
array<float> &out_pixels,
string &error)
{
alloc_pixels(out_spec, out_pixels);
memset(out_pixels.data(), 0, out_pixels.size() * sizeof(float));
for (const MergeImage &image : images) {
/* Read all channels into buffer. Reading all channels at once is
* faster than individually due to interleaved EXR channel storage. */
array<float> pixels;
alloc_pixels(image.in->spec(), pixels);
const int num_channels = image.in->spec().nchannels;
if (!image.in->read_image(0, 0, 0, num_channels, TypeDesc::FLOAT, pixels.data())) {
error = "Failed to read image: " + image.filepath;
return false;
}
for (const MergeImageLayer &layer : image.layers) {
const size_t stride = image.in->spec().nchannels;
const size_t out_stride = out_spec.nchannels;
const size_t num_pixels = pixels.size();
for (const MergeImagePass &pass : layer.passes) {
size_t offset = pass.offset;
size_t out_offset = pass.merge_offset;
switch (pass.op) {
case MERGE_CHANNEL_NOP:
break;
case MERGE_CHANNEL_COPY:
for (; offset < num_pixels; offset += stride, out_offset += out_stride) {
out_pixels[out_offset] = pixels[offset];
}
break;
case MERGE_CHANNEL_SUM:
for (; offset < num_pixels; offset += stride, out_offset += out_stride) {
out_pixels[out_offset] += pixels[offset];
}
break;
case MERGE_CHANNEL_AVERAGE: {
/* Weights based on sample count passes and sample metadata. Per channel since not
* all files are guaranteed to have the same channels. */
size_t sample_pass_offset = layer.sample_pass_offset;
const auto &samples = layer_samples.at(layer.name);
for (size_t i = 0; offset < num_pixels;
offset += stride, sample_pass_offset += stride, out_offset += out_stride, i++)
{
const float total_samples = samples.per_pixel[i];
float layer_samples;
if (layer.has_sample_pass) {
layer_samples = pixels[sample_pass_offset] * layer.samples;
}
else {
layer_samples = layer.samples;
}
out_pixels[out_offset] += pixels[offset] * (1.0f * layer_samples / total_samples);
}
break;
}
case MERGE_CHANNEL_SAMPLES: {
const auto &samples = layer_samples.at(layer.name);
for (size_t i = 0; offset < num_pixels;
offset += stride, out_offset += out_stride, i++)
{
out_pixels[out_offset] = 1.0f * samples.per_pixel[i] / samples.total;
}
break;
}
}
}
}
}
return true;
}
static bool save_output(const string &filepath,
const ImageSpec &spec,
const array<float> &pixels,
string &error)
{
/* Write to temporary file path, so we merge images in place and don't
* risk destroying files when something goes wrong in file saving. */
const string extension = OIIO::Filesystem::extension(filepath);
const string unique_name = ".merge-tmp-" + OIIO::Filesystem::unique_path();
const string tmp_filepath = filepath + unique_name + extension;
unique_ptr<ImageOutput> out(ImageOutput::create(tmp_filepath));
if (!out) {
error = "Failed to open temporary file " + tmp_filepath + " for writing";
return false;
}
/* Open temporary file and write image buffers. */
if (!out->open(tmp_filepath, spec)) {
error = "Failed to open file " + tmp_filepath + " for writing: " + out->geterror();
return false;
}
bool ok = true;
if (!out->write_image(TypeDesc::FLOAT, pixels.data())) {
error = "Failed to write to file " + tmp_filepath + ": " + out->geterror();
ok = false;
}
if (!out->close()) {
error = "Failed to save to file " + tmp_filepath + ": " + out->geterror();
ok = false;
}
out.reset();
/* Copy temporary file to output filepath. */
string rename_error;
if (ok && !OIIO::Filesystem::rename(tmp_filepath, filepath, rename_error)) {
error = "Failed to move merged image to " + filepath + ": " + rename_error;
ok = false;
}
if (!ok) {
OIIO::Filesystem::remove(tmp_filepath);
}
return ok;
}
static void read_layer_samples(vector<MergeImage> &images,
unordered_map<string, SampleCount> &layer_samples)
{
for (auto &image : images) {
const ImageSpec &in_spec = image.in->spec();
for (auto &layer : image.layers) {
const bool initialize = (!layer_samples.contains(layer.name));
auto &current_layer_samples = layer_samples[layer.name];
if (initialize) {
current_layer_samples.total = 0;
current_layer_samples.per_pixel.resize(in_spec.width * in_spec.height);
std::fill(
current_layer_samples.per_pixel.begin(), current_layer_samples.per_pixel.end(), 0.0f);
}
if (layer.has_sample_pass) {
/* Load the "Debug Sample Count" pass and add the samples to the layer's sample count. */
array<float> sample_count_buffer;
sample_count_buffer.resize(in_spec.width * in_spec.height);
image.in->read_image(0,
0,
layer.sample_pass_offset,
layer.sample_pass_offset,
TypeDesc::FLOAT,
(void *)sample_count_buffer.data());
for (size_t i = 0; i < current_layer_samples.per_pixel.size(); i++) {
current_layer_samples.per_pixel[i] += sample_count_buffer[i] * layer.samples;
}
}
else {
/* Use sample count from metadata if there's no "Debug Sample Count" pass. */
for (size_t i = 0; i < current_layer_samples.per_pixel.size(); i++) {
current_layer_samples.per_pixel[i] += layer.samples;
}
}
current_layer_samples.total += layer.samples;
}
}
}
/* Image Merger */
ImageMerger::ImageMerger() = default;
bool ImageMerger::run()
{
if (input.empty()) {
error = "No input file paths specified.";
return false;
}
if (output.empty()) {
error = "No output file path specified.";
return false;
}
/* Open images and verify they have matching layout. */
vector<MergeImage> images;
if (!open_images(input, images, error)) {
return false;
}
/* Load and sum sample count for each render layer. */
unordered_map<string, SampleCount> layer_samples;
read_layer_samples(images, layer_samples);
/* Merge metadata and setup channels and offsets. */
ImageSpec out_spec;
merge_channels_metadata(images, out_spec);
/* Merge pixels. */
array<float> out_pixels;
if (!merge_pixels(images, out_spec, layer_samples, out_pixels, error)) {
return false;
}
/* We don't need input anymore at this point, and will possibly
* overwrite the same file. */
images.clear();
/* Save output file. */
return save_output(output, out_spec, out_pixels, error);
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,28 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "util/string.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
/* Merge OpenEXR multi-layer renders. */
class ImageMerger {
public:
ImageMerger();
bool run();
/* Error message after running, in case of failure. */
string error;
/* List of image filepaths to merge. */
vector<string> input;
/* Output filepath. */
string output;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,70 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "util/math.h"
#include "util/string.h"
#include "util/types.h"
CCL_NAMESPACE_BEGIN
/* Output driver for reading render buffers.
*
* Host applications implement this interface for outputting render buffers for offline rendering.
* Drivers can be used to copy the buffers into the host application or write them directly to
* disk. This interface may also be used for interactive display, however the DisplayDriver is more
* efficient for that purpose.
*/
class OutputDriver {
public:
OutputDriver() = default;
virtual ~OutputDriver() = default;
class Tile {
public:
Tile(const int2 offset,
const int2 size,
const int2 full_size,
const string_view layer,
const string_view view)
: offset(offset), size(size), full_size(full_size), layer(layer), view(view)
{
}
virtual ~Tile() = default;
const int2 offset;
const int2 size;
const int2 full_size;
const string layer;
const string view;
virtual bool get_pass_pixels(const string_view pass_name,
const int num_channels,
float *pixels) const = 0;
virtual bool set_pass_pixels(const string_view pass_name,
const int num_channels,
const float *pixels) const = 0;
};
/* Write tile once it has finished rendering. */
virtual void write_render_tile(const Tile &tile) = 0;
/* Update tile while rendering is in progress. Return true if any update
* was performed. */
virtual bool update_render_tile(const Tile & /* tile */)
{
return false;
}
/* For baking, read render pass PASS_BAKE_PRIMITIVE/SEED/DIFFERENTIAL
* to determine which shading points to use for baking at each pixel. Return
* true if any data was read. */
virtual bool read_render_tile(const Tile & /* tile */)
{
return false;
}
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,815 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include <cstring>
#include "device/cpu/device.h"
#include "device/device.h"
#include "integrator/path_trace.h"
#include "scene/background.h"
#include "scene/camera.h"
#include "scene/image.h"
#include "scene/integrator.h"
#include "scene/light.h"
#include "scene/mesh.h"
#include "scene/object.h"
#include "scene/scene.h"
#include "scene/shader_graph.h"
#include "session/buffers.h"
#include "session/display_driver.h"
#include "session/output_driver.h"
#include "session/session.h"
#include "util/log.h"
#include "util/math.h"
#include "util/task.h"
#include "util/time.h"
CCL_NAMESPACE_BEGIN
Session::Session(const SessionParams &params_, const SceneParams &scene_params)
: params(params_),
eviction_manager_(params_.background),
render_scheduler_(tile_manager_, params)
{
TaskScheduler::init(params.threads);
delayed_reset_.do_reset = false;
pause_ = false;
new_work_added_ = false;
device = Device::create(params.device, stats, profiler, params_.headless);
if (device->have_error()) {
progress.set_error(device->error_message());
}
scene = make_unique<Scene>(scene_params, device.get());
if (params.device == params.denoise_device) {
/* Reuse render device. */
}
else {
denoise_device_ = Device::create(params.denoise_device, stats, profiler, params_.headless);
if (denoise_device_->have_error()) {
progress.set_error(denoise_device_->error_message());
}
}
/* Configure path tracer. */
path_trace_ = make_unique<PathTrace>(device.get(),
denoise_device(),
scene->film,
&scene->dscene,
render_scheduler_,
tile_manager_);
path_trace_->set_progress(&progress);
path_trace_->progress_update_cb = [&]() { update_status_time(); };
tile_manager_.full_buffer_written_cb = [&](string_view filename) {
if (!full_buffer_written_cb) {
return;
}
full_buffer_written_cb(filename);
};
/* Create session thread. */
session_thread_ = make_unique<thread>([this] { thread_run(); });
}
Session::~Session()
{
/* Cancel any ongoing render operation. */
cancel();
/* Signal session thread to end. */
{
const thread_scoped_lock session_thread_lock(session_thread_mutex_);
session_thread_state_ = SESSION_THREAD_END;
}
session_thread_cond_.notify_all();
/* Destroy session thread. */
session_thread_->join();
session_thread_.reset();
/* Destroy path tracer, before the device. This is needed because destruction might need to
* access device for device memory free.
* TODO(sergey): Convert device to be unique_ptr, and rely on C++ to destruct objects in the
* pre-defined order. */
path_trace_.reset();
/* Destroy scene and device. */
scene.reset();
denoise_device_.reset();
device.reset();
/* Stop task scheduler. */
TaskScheduler::exit();
}
void Session::start()
{
{
/* Signal session thread to start rendering. */
const thread_scoped_lock session_thread_lock(session_thread_mutex_);
if (session_thread_state_ == SESSION_THREAD_RENDER) {
/* Already rendering, nothing to do. */
return;
}
session_thread_state_ = SESSION_THREAD_RENDER;
}
session_thread_cond_.notify_all();
}
void Session::cancel(bool quick)
{
/* Cancel any long running device operations (e.g. shader compilations). */
device->cancel();
/* Check if session thread is rendering. */
const bool rendering = is_session_thread_rendering();
if (rendering) {
/* Cancel path trace operations. */
if (quick && path_trace_) {
path_trace_->cancel();
}
/* Cancel other operations. */
progress.set_cancel("Exiting");
/* Signal unpause in case the render was paused. */
{
const thread_scoped_lock pause_lock(pause_mutex_);
pause_ = false;
}
pause_cond_.notify_all();
/* Wait for render thread to be cancelled or finished. */
wait();
}
}
bool Session::ready_to_reset()
{
return path_trace_->ready_to_reset();
}
void Session::run_main_render_loop()
{
path_trace_->zero_display();
while (true) {
RenderWork render_work = run_update_for_next_iteration();
const bool did_cancel = progress.get_cancel();
if (!render_work) {
if (LOG_IS_ON(LOG_LEVEL_INFO)) {
if (did_cancel) {
LOG_INFO << "Rendering was canceled.";
}
else {
double total_time;
double render_time;
progress.get_time(total_time, render_time);
LOG_INFO << "Rendering in main loop is done in " << render_time << " seconds.";
LOG_INFO << path_trace_->full_report();
}
}
if (params.background) {
/* if no work left and in background mode, we can stop immediately. */
progress.set_status("Finished");
break;
}
}
if (did_cancel) {
render_scheduler_.render_work_reschedule_on_cancel(render_work);
if (!render_work) {
break;
}
}
else if (run_wait_for_work(render_work)) {
continue;
}
/* Stop rendering if error happened during scene update or other step of preparing scene
* for render. */
if (device->have_error()) {
progress.set_error(device->error_message());
break;
}
{
/* buffers mutex is locked entirely while rendering each
* sample, and released/reacquired on each iteration to allow
* reset and draw in between */
const thread_scoped_lock buffers_lock(buffers_mutex_);
/* update status and timing */
update_status_time();
/* render */
path_trace_->render(render_work);
/* update status and timing */
update_status_time();
/* Stop rendering if error happened during path tracing. */
if (device->have_error()) {
progress.set_error(device->error_message());
break;
}
}
progress.set_update();
if (did_cancel) {
break;
}
}
}
void Session::thread_run()
{
while (true) {
{
thread_scoped_lock session_thread_lock(session_thread_mutex_);
if (session_thread_state_ == SESSION_THREAD_WAIT) {
/* Continue waiting for any signal from the main thread. */
session_thread_cond_.wait(session_thread_lock);
continue;
}
if (session_thread_state_ == SESSION_THREAD_END) {
/* End thread immediately. */
break;
}
}
/* Execute a render. */
thread_render();
/* Go back from rendering to waiting. */
{
const thread_scoped_lock session_thread_lock(session_thread_mutex_);
if (session_thread_state_ == SESSION_THREAD_RENDER) {
session_thread_state_ = SESSION_THREAD_WAIT;
}
}
session_thread_cond_.notify_all();
}
/* Flush any remaining operations and destroy display driver here. This ensure
* graphics API resources are created and destroyed all in the session thread,
* which can avoid problems contexts and multiple threads. */
path_trace_->flush_display();
path_trace_->set_display_driver(nullptr);
}
void Session::thread_render()
{
if (params.use_profiling && (params.device.type == DEVICE_CPU)) {
profiler.start();
}
/* session thread loop */
progress.set_status("Waiting for render to start");
/* run */
if (!progress.get_cancel()) {
/* reset number of rendered samples */
progress.reset_sample();
run_main_render_loop();
}
profiler.stop();
/* progress update */
if (progress.get_cancel()) {
progress.set_status(progress.get_cancel_message());
}
else {
progress.set_update();
}
}
bool Session::is_session_thread_rendering()
{
const thread_scoped_lock session_thread_lock(session_thread_mutex_);
return (session_thread_state_ == SESSION_THREAD_RENDER);
}
RenderWork Session::run_update_for_next_iteration()
{
RenderWork render_work;
thread_scoped_lock scene_lock(scene->mutex);
/* Perform delayed reset if requested. */
const bool reset_buffers = delayed_reset_buffer_params();
/* Update scene */
const bool reset_scene = update_scene(delayed_reset_.do_reset);
/* Update buffers for new parameters. After scene update which influences the passes used. */
bool have_tiles = true;
bool switched_to_new_tile = false;
if (reset_buffers) {
update_buffers_for_params();
/* After reset make sure the tile manager is at the first big tile. */
have_tiles = tile_manager_.next();
switched_to_new_tile = true;
eviction_manager_.reset();
}
/* Update denoiser settings. */
{
const DenoiseParams denoise_params = scene->integrator->get_denoise_params();
path_trace_->set_denoiser_params(denoise_params);
}
/* Update adaptive sampling. */
{
const AdaptiveSampling adaptive_sampling = scene->integrator->get_adaptive_sampling();
path_trace_->set_adaptive_sampling(adaptive_sampling);
}
/* Update path guiding. */
{
const GuidingParams guiding_params = scene->integrator->get_guiding_params(device.get());
const bool guiding_reset = (guiding_params.use) ? reset_scene : false;
path_trace_->set_guiding_params(guiding_params, guiding_reset);
}
render_scheduler_.set_sample_params(params.samples,
params.use_sample_subset,
params.sample_subset_offset,
params.sample_subset_length);
render_scheduler_.set_time_limit(params.time_limit);
while (have_tiles) {
render_work = render_scheduler_.get_render_work();
if (render_work) {
break;
}
progress.add_finished_tile(false);
have_tiles = tile_manager_.next();
if (have_tiles) {
render_scheduler_.reset_for_next_tile();
switched_to_new_tile = true;
}
}
/* Evict unused image tiles periodically. */
if (eviction_manager_.need_eviction(!render_work, switched_to_new_tile)) {
scene->image_manager->evict_unused(device.get(), scene.get());
}
if (render_work) {
const scoped_timer update_timer;
if (switched_to_new_tile) {
BufferParams tile_params = buffer_params_;
const Tile &tile = tile_manager_.get_current_tile();
tile_params.width = tile.width;
tile_params.height = tile.height;
tile_params.window_x = tile.window_x;
tile_params.window_y = tile.window_y;
tile_params.window_width = tile.window_width;
tile_params.window_height = tile.window_height;
tile_params.full_x = tile.x + buffer_params_.full_x;
tile_params.full_y = tile.y + buffer_params_.full_y;
tile_params.full_width = buffer_params_.full_width;
tile_params.full_height = buffer_params_.full_height;
tile_params.update_offset_stride();
path_trace_->reset(buffer_params_, tile_params, reset_buffers);
}
/* Update camera if dimensions changed for progressive render. the camera
* knows nothing about progressive or cropped rendering, it just gets the
* image dimensions passed in. */
const float resolution = render_work.resolution_divider;
const int width = max(1, int(buffer_params_.full_width / resolution));
const int height = max(1, int(buffer_params_.full_height / resolution));
scene->update_camera_resolution(progress, width, height);
/* Unlock scene mutex before loading denoiser kernels, since that may attempt to activate
* graphics interop, which can deadlock when the scene mutex is still being held. */
scene_lock.unlock();
path_trace_->load_kernels();
path_trace_->alloc_work_memory();
/* Wait for device to be ready (e.g. finish any background compilations). */
string device_status;
while (!device->is_ready(device_status)) {
progress.set_status(device_status);
if (progress.get_cancel()) {
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(200));
}
progress.add_skip_time(update_timer, params.background);
}
return render_work;
}
bool Session::run_wait_for_work(const RenderWork &render_work)
{
/* In an offline rendering there is no pause, and no tiles will mean the job is fully done. */
if (params.background) {
return false;
}
thread_scoped_lock pause_lock(pause_mutex_);
if (!pause_ && render_work) {
/* Rendering is not paused and there is work to be done. No need to wait for anything. */
return false;
}
const bool no_work = !render_work;
update_status_time(pause_, no_work);
/* Only leave the loop when rendering is not paused. But even if the current render is
* un-paused but there is nothing to render keep waiting until new work is added. */
while (!progress.get_cancel()) {
const scoped_timer pause_timer;
if (!pause_ && (render_work || new_work_added_ || delayed_reset_.do_reset)) {
break;
}
const std::chrono::milliseconds wait_time = eviction_manager_.wait_time(!render_work);
if (wait_time == std::chrono::milliseconds::zero()) {
/* Break out of the loop for cache eviction. */
break;
}
/* Wait for either pause state changed, extra samples added to render, or idle
* timer before performing eviction. */
if (wait_time == std::chrono::milliseconds::max()) {
pause_cond_.wait(pause_lock);
}
else {
pause_cond_.wait_for(pause_lock, wait_time);
}
if (pause_) {
progress.add_skip_time(pause_timer, params.background);
}
update_status_time(pause_, no_work);
progress.set_update();
}
new_work_added_ = false;
return no_work;
}
void Session::draw()
{
path_trace_->draw();
}
int2 Session::get_effective_tile_size() const
{
const int image_width = buffer_params_.width;
const int image_height = buffer_params_.height;
if (!params.use_auto_tile) {
return make_int2(image_width, image_height);
}
const int64_t image_area = static_cast<int64_t>(image_width) * image_height;
/* TODO(sergey): Take available memory into account, and if there is enough memory do not
* tile and prefer optimal performance. */
const int tile_size = tile_manager_.compute_render_tile_size(params.tile_size);
const int64_t actual_tile_area = static_cast<int64_t>(tile_size) * tile_size;
if (actual_tile_area >= image_area && image_width <= TileManager::MAX_TILE_SIZE &&
image_height <= TileManager::MAX_TILE_SIZE)
{
return make_int2(image_width, image_height);
}
return make_int2(tile_size, tile_size);
}
bool Session::delayed_reset_buffer_params()
{
/* Reset buffer parameters, delayed from when we got the reset call so we can complete
* rendering the sample. Otherwise e.g. viewport navigation might reset without ever
* finishing anything. */
const thread_scoped_lock reset_lock(delayed_reset_.mutex);
if (!delayed_reset_.do_reset) {
return false;
}
const thread_scoped_lock buffers_lock(buffers_mutex_);
delayed_reset_.do_reset = false;
params = delayed_reset_.session_params;
buffer_params_ = delayed_reset_.buffer_params;
/* Store parameters used for buffers access outside of scene graph. */
buffer_params_.samples = min(params.samples, Integrator::MAX_SAMPLES);
buffer_params_.exposure = scene->film->get_exposure();
buffer_params_.use_approximate_shadow_catcher =
scene->film->get_use_approximate_shadow_catcher();
buffer_params_.use_transparent_background = scene->background->get_transparent();
/* Tile and work scheduling. */
tile_manager_.reset_scheduling(buffer_params_, get_effective_tile_size());
return true;
}
void Session::update_buffers_for_params()
{
render_scheduler_.set_sample_params(params.samples,
params.use_sample_subset,
params.sample_subset_offset,
params.sample_subset_length);
render_scheduler_.reset(buffer_params_);
/* Update for new state of scene and passes. */
buffer_params_.update_passes(scene->passes);
tile_manager_.update(buffer_params_, scene.get());
/* Update temp directory on reset.
* This potentially allows to finish the existing rendering with a previously configure
* temporary
* directory in the host software and switch to a new temp directory when new render starts. */
tile_manager_.set_temp_dir(params.temp_dir);
/* Progress. */
progress.reset_sample();
progress.set_total_pixel_samples(static_cast<uint64_t>(buffer_params_.width) *
buffer_params_.height * buffer_params_.samples);
if (!params.background) {
progress.set_start_time();
}
const double time_limit = params.time_limit * ((double)tile_manager_.get_num_tiles());
progress.set_render_start_time();
progress.set_time_limit(time_limit);
}
void Session::reset(const SessionParams &session_params, const BufferParams &buffer_params)
{
{
const thread_scoped_lock reset_lock(delayed_reset_.mutex);
const thread_scoped_lock pause_lock(pause_mutex_);
delayed_reset_.do_reset = true;
delayed_reset_.session_params = session_params;
delayed_reset_.buffer_params = buffer_params;
scene->scene_updated_while_loading_kernels = true;
path_trace_->cancel();
}
pause_cond_.notify_all();
}
void Session::set_samples(const int samples)
{
if (samples == params.samples) {
return;
}
params.samples = samples;
{
const thread_scoped_lock pause_lock(pause_mutex_);
new_work_added_ = true;
}
pause_cond_.notify_all();
}
void Session::set_time_limit(const double time_limit)
{
if (time_limit == params.time_limit) {
return;
}
params.time_limit = time_limit;
{
const thread_scoped_lock pause_lock(pause_mutex_);
new_work_added_ = true;
}
pause_cond_.notify_all();
}
void Session::set_pause(bool pause)
{
bool notify = false;
{
const thread_scoped_lock pause_lock(pause_mutex_);
if (pause != pause_) {
pause_ = pause;
notify = true;
}
}
if (is_session_thread_rendering()) {
if (notify) {
pause_cond_.notify_all();
}
}
else if (pause_) {
update_status_time(pause_);
}
}
void Session::set_navigating(bool navigating)
{
eviction_manager_.set_navigating(navigating);
}
void Session::set_output_driver(unique_ptr<OutputDriver> driver)
{
path_trace_->set_output_driver(std::move(driver));
}
void Session::set_display_driver(unique_ptr<DisplayDriver> driver)
{
path_trace_->set_display_driver(std::move(driver));
}
double Session::get_estimated_remaining_time() const
{
const double completed = progress.get_progress();
if (completed == 0.0) {
return 0.0;
}
double total_time;
double render_time;
progress.get_time(total_time, render_time);
double remaining = (1.0 - (double)completed) * (render_time / (double)completed);
const double time_limit = render_scheduler_.get_time_limit() *
((double)tile_manager_.get_num_tiles());
if (time_limit != 0.0) {
remaining = min(remaining, max(time_limit - render_time, 0.0));
}
return remaining;
}
void Session::wait()
{
/* Wait until session thread either is waiting or ending. */
while (true) {
thread_scoped_lock session_thread_lock(session_thread_mutex_);
if (session_thread_state_ != SESSION_THREAD_RENDER) {
break;
}
session_thread_cond_.wait(session_thread_lock);
}
}
bool Session::update_scene(const bool reset_samples)
{
/* Update number of samples in the integrator.
* Ideally this would need to happen once in `Session::set_samples()`, but the issue there is
* the initial configuration when Session is created where the `set_samples()` is not used.
*
* NOTE: Unless reset was requested only allow increasing number of samples. */
if (reset_samples || scene->integrator->get_aa_samples() < params.samples) {
scene->integrator->set_aa_samples(params.samples);
}
scene->integrator->set_use_sample_subset(params.use_sample_subset);
scene->integrator->set_sample_subset_offset(params.sample_subset_offset);
scene->integrator->set_sample_subset_length(params.sample_subset_length);
/* When multiple tiles are used SAMPLE_COUNT pass is used to keep track of possible partial
* tile results. */
scene->film->set_use_sample_count(tile_manager_.has_multiple_tiles());
const bool reset = scene->need_reset(false);
if (scene->update(progress)) {
profiler.reset(scene->shaders.size(), scene->objects.size());
}
return reset;
}
static string status_append(const string &status, const string &suffix)
{
string prefix = status;
if (!prefix.empty()) {
prefix += ", ";
}
return prefix + suffix;
}
void Session::update_status_time(bool show_pause, bool show_done)
{
string status;
string substatus;
const int current_tile = progress.get_rendered_tiles();
const int num_tiles = tile_manager_.get_num_tiles();
const int current_sample = progress.get_current_sample();
const int num_samples = render_scheduler_.get_num_samples();
/* TIle. */
if (tile_manager_.has_multiple_tiles()) {
substatus = status_append(substatus,
string_printf("Rendered %d/%d Tiles", current_tile, num_tiles));
}
/* Sample. */
if (!params.background && num_samples == Integrator::MAX_SAMPLES) {
substatus = status_append(substatus, string_printf("Sample %d", current_sample));
}
else {
substatus = status_append(substatus,
string_printf("Sample %d/%d", current_sample, num_samples));
}
/* Append any device-specific status (such as background kernel optimization) */
string device_status;
if (device->is_ready(device_status) && !device_status.empty()) {
substatus += string_printf(" (%s)", device_status.c_str());
}
/* TODO(sergey): Denoising status from the path trace. */
if (show_pause) {
status = "Rendering Paused";
}
else if (show_done) {
status = "Rendering Done";
progress.set_end_time(); /* Save end time so that further calls to get_time are accurate. */
}
else {
status = substatus;
substatus.clear();
}
progress.set_status(status, substatus);
}
void Session::device_free()
{
scene->device_free();
path_trace_->device_free();
}
void Session::collect_statistics(RenderStats *render_stats)
{
scene->collect_statistics(render_stats);
if (params.use_profiling && (params.device.type == DEVICE_CPU)) {
render_stats->collect_profiling(scene.get(), profiler);
}
}
/* --------------------------------------------------------------------
* Full-frame on-disk storage.
*/
void Session::process_full_buffer_from_disk(string_view filename)
{
path_trace_->process_full_buffer_from_disk(filename);
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,259 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <functional>
#include "device/device.h"
#include "integrator/render_scheduler.h"
#include "scene/shader.h"
#include "scene/stats.h"
#include "session/buffers.h"
#include "session/cache_eviction.h"
#include "session/tile.h"
#include "util/progress.h"
#include "util/stats.h"
#include "util/thread.h"
#include "util/unique_ptr.h"
CCL_NAMESPACE_BEGIN
class BufferParams;
class Device;
class DeviceScene;
class DisplayDriver;
class OutputDriver;
class PathTrace;
class Progress;
class RenderBuffers;
class Scene;
class SceneParams;
/* Session Parameters */
class SessionParams {
public:
/* Device, which is chosen based on Blender Cycles preferences, as well as Scene settings and
* command line arguments. */
DeviceInfo device;
/* Device from Cycles preferences for denoising. */
DeviceInfo denoise_device;
bool headless;
bool background;
int samples;
bool use_sample_subset;
int sample_subset_offset;
int sample_subset_length;
int pixel_size;
int threads;
/* Limit in seconds for how long path tracing is allowed to happen.
* Zero means no limit is applied. */
double time_limit;
bool use_profiling;
bool use_auto_tile;
int tile_size;
bool use_resolution_divider;
ShadingSystem shadingsystem;
/* Session-specific temporary directory to store in-progress EXR files in. */
string temp_dir;
SessionParams()
{
headless = false;
background = false;
samples = 1024;
use_sample_subset = false;
sample_subset_offset = 0;
sample_subset_length = 1024;
pixel_size = 1;
threads = 0;
time_limit = 0.0;
use_profiling = false;
use_auto_tile = true;
tile_size = 2048;
use_resolution_divider = true;
shadingsystem = SHADINGSYSTEM_SVM;
}
bool modified(const SessionParams &params) const
{
/* Modified means we have to recreate the session, any parameter changes
* that can be handled by an existing Session are omitted. */
return !(device == params.device && headless == params.headless &&
background == params.background && pixel_size == params.pixel_size &&
threads == params.threads && use_profiling == params.use_profiling &&
use_auto_tile == params.use_auto_tile && tile_size == params.tile_size &&
use_resolution_divider == params.use_resolution_divider &&
shadingsystem == params.shadingsystem);
}
};
/* Session
*
* This is the class that contains the session thread, running the render
* control loop and dispatching tasks. */
class Session {
public:
unique_ptr<Device> device;
/* Denoiser device. Could be the same as the path trace device. */
unique_ptr<Device> denoise_device_;
unique_ptr<Scene> scene;
Progress progress;
SessionParams params;
Stats stats;
Profiler profiler;
/* Callback is invoked by tile manager whenever on-dist tiles storage file is closed after
* writing. Allows an engine integration to keep track of those files without worry about
* transferring the information when it needs to re-create session during rendering. */
std::function<void(string_view)> full_buffer_written_cb;
explicit Session(const SessionParams &params, const SceneParams &scene_params);
~Session();
void start();
/* When quick cancel is requested path tracing is cancels as soon as possible, without waiting
* for the buffer to be uniformly sampled. */
void cancel(bool quick = false);
void draw();
void wait();
bool ready_to_reset();
void reset(const SessionParams &session_params, const BufferParams &buffer_params);
void set_pause(bool pause);
void set_navigating(bool navigating);
void set_samples(const int samples);
void set_time_limit(const double time_limit);
void set_output_driver(unique_ptr<OutputDriver> driver);
void set_display_driver(unique_ptr<DisplayDriver> driver);
double get_estimated_remaining_time() const;
void device_free();
/* Returns the rendering progress or 0 if no progress can be determined
* (for example, when rendering with unlimited samples). */
float get_progress();
void collect_statistics(RenderStats *stats);
/* --------------------------------------------------------------------
* Full-frame on-disk storage.
*/
/* Read given full-frame file from disk, perform needed processing and write it to the software
* via the write callback. */
void process_full_buffer_from_disk(string_view filename);
protected:
struct DelayedReset {
thread_mutex mutex;
bool do_reset;
SessionParams session_params;
BufferParams buffer_params;
} delayed_reset_;
void thread_run();
void thread_render();
/* Check whether the session thread is in `SESSION_THREAD_RENDER` state.
* Returns true if it is so. */
bool is_session_thread_rendering();
/* Update for the new iteration of the main loop in run implementation (run_cpu and run_gpu).
*
* Will take care of the following things:
* - Delayed reset
* - Scene update
* - Tile manager advance
* - Render scheduler work request
*
* The updates are done in a proper order with proper locking around them, which guarantees
* that the device side of scene and render buffers are always in a consistent state.
*
* Returns render work which is to be rendered next. */
RenderWork run_update_for_next_iteration();
/* Wait for rendering to be unpaused, or for new tiles for render to arrive.
* Returns true if new main render loop iteration is required after this function call.
*
* The `render_work` is the work which was scheduled by the render scheduler right before
* checking the pause. */
bool run_wait_for_work(const RenderWork &render_work);
void run_main_render_loop();
bool update_scene(const bool reset_samples);
void update_status_time(bool show_pause = false, bool show_done = false);
bool delayed_reset_buffer_params();
void update_buffers_for_params();
int2 get_effective_tile_size() const;
/* Get device used for denoising, may be the same as render device. */
Device *denoise_device()
{
return (denoise_device_) ? denoise_device_.get() : device.get();
}
/* Session thread that performs rendering tasks decoupled from the thread
* controlling the sessions. The thread is created and destroyed along with
* the session. */
unique_ptr<thread> session_thread_ = nullptr;
thread_condition_variable session_thread_cond_;
thread_mutex session_thread_mutex_;
enum {
SESSION_THREAD_WAIT,
SESSION_THREAD_RENDER,
SESSION_THREAD_END,
} session_thread_state_ = SESSION_THREAD_WAIT;
bool pause_ = false;
bool new_work_added_ = false;
thread_condition_variable pause_cond_;
thread_mutex pause_mutex_;
thread_mutex tile_mutex_;
thread_mutex buffers_mutex_;
TileManager tile_manager_;
BufferParams buffer_params_;
/* Manages when image cache eviction happens. */
CacheEvictionManager eviction_manager_;
/* Render scheduler is used to get work to be rendered with the current big tile. */
RenderScheduler render_scheduler_;
/* Path tracer object.
*
* Is a single full-frame path tracer for interactive viewport rendering.
* A path tracer for the current big-tile for an offline rendering. */
unique_ptr<PathTrace> path_trace_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,664 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "session/tile.h"
#include <atomic>
#include "graph/node.h"
#include "scene/background.h"
#include "scene/bake.h"
#include "scene/film.h"
#include "scene/integrator.h"
#include "scene/scene.h"
#include "session/session.h"
#include "util/log.h"
#include "util/path.h"
#include "util/string.h"
#include "util/system.h"
#include "util/time.h"
#include "util/types.h"
CCL_NAMESPACE_BEGIN
/* --------------------------------------------------------------------
* Internal functions.
*/
static const char *ATTR_PASSES_COUNT = "cycles.passes.count";
static const char *ATTR_PASS_SOCKET_PREFIX_FORMAT = "cycles.passes.%d.";
static const char *ATTR_BUFFER_SOCKET_PREFIX = "cycles.buffer.";
static const char *ATTR_DENOISE_SOCKET_PREFIX = "cycles.denoise.";
/* Global counter of ToleManager object instances. */
static std::atomic<uint64_t> g_instance_index = 0;
/* Construct names of EXR channels which will ensure order of all channels to match exact offsets
* in render buffers corresponding to the given passes.
*
* Returns `std` data-types so that it can be assigned directly to the OIIO's `ImageSpec`. */
static std::vector<std::string> exr_channel_names_for_passes(const BufferParams &buffer_params)
{
static const char *component_suffixes[] = {"R", "G", "B", "A"};
int pass_index = 0;
std::vector<std::string> channel_names;
for (const BufferPass &pass : buffer_params.passes) {
if (pass.offset == PASS_UNUSED) {
continue;
}
const PassInfo pass_info = pass.get_info();
/* EXR canonically expects first part of channel names to be sorted alphabetically, which is
* not guaranteed to be the case with passes names. Assign a prefix based on the pass index
* with a fixed width to ensure ordering. This makes it possible to dump existing render
* buffers memory to disk and read it back without doing extra mapping. */
const string prefix = string_printf("%08d", pass_index);
const string channel_name_prefix = prefix + string(pass.name) + ".";
for (int i = 0; i < pass_info.num_components; ++i) {
channel_names.push_back(channel_name_prefix + component_suffixes[i]);
}
++pass_index;
}
return channel_names;
}
inline string node_socket_attribute_name(const SocketType &socket, const string &attr_name_prefix)
{
return attr_name_prefix + string(socket.name);
}
template<typename ValidateValueFunc, typename GetValueFunc>
static bool node_socket_generic_to_image_spec_atttributes(
ImageSpec *image_spec,
const Node *node,
const SocketType &socket,
const string &attr_name_prefix,
const ValidateValueFunc &validate_value_func,
const GetValueFunc &get_value_func)
{
if (!validate_value_func(node, socket)) {
return false;
}
image_spec->attribute(node_socket_attribute_name(socket, attr_name_prefix),
get_value_func(node, socket));
return true;
}
static bool node_socket_to_image_spec_atttributes(ImageSpec *image_spec,
const Node *node,
const SocketType &socket,
const string &attr_name_prefix)
{
const string attr_name = node_socket_attribute_name(socket, attr_name_prefix);
switch (socket.type) {
case SocketType::ENUM: {
const ustring value = node->get_string(socket);
/* Validate that the node is consistent with the node type definition. */
const NodeEnum &enum_values = *socket.enum_values;
if (!enum_values.exists(value)) {
LOG_DFATAL << "Node enum contains invalid value " << value;
return false;
}
image_spec->attribute(attr_name, value);
return true;
}
case SocketType::STRING:
image_spec->attribute(attr_name, node->get_string(socket));
return true;
case SocketType::INT:
image_spec->attribute(attr_name, node->get_int(socket));
return true;
case SocketType::FLOAT:
image_spec->attribute(attr_name, node->get_float(socket));
return true;
case SocketType::BOOLEAN:
image_spec->attribute(attr_name, node->get_bool(socket));
return true;
default:
LOG_DFATAL << "Unhandled socket type " << socket.type << ", should never happen.";
return false;
}
}
static bool node_socket_from_image_spec_atttributes(Node *node,
const SocketType &socket,
const ImageSpec &image_spec,
const string &attr_name_prefix)
{
const string attr_name = node_socket_attribute_name(socket, attr_name_prefix);
switch (socket.type) {
case SocketType::ENUM: {
/* TODO(sergey): Avoid construction of `ustring` by using `string_view` in the Node API. */
const ustring value(image_spec.get_string_attribute(attr_name, ""));
/* Validate that the node is consistent with the node type definition. */
const NodeEnum &enum_values = *socket.enum_values;
if (!enum_values.exists(value)) {
LOG_ERROR << "Invalid enumerator value " << value;
return false;
}
node->set(socket, enum_values[value]);
return true;
}
case SocketType::STRING:
/* TODO(sergey): Avoid construction of `ustring` by using `string_view` in the Node API. */
node->set(socket, ustring(image_spec.get_string_attribute(attr_name, "")));
return true;
case SocketType::INT:
node->set(socket, image_spec.get_int_attribute(attr_name, 0));
return true;
case SocketType::FLOAT:
node->set(socket, image_spec.get_float_attribute(attr_name, 0));
return true;
case SocketType::BOOLEAN:
node->set(socket, static_cast<bool>(image_spec.get_int_attribute(attr_name, 0)));
return true;
default:
LOG_DFATAL << "Unhandled socket type " << socket.type << ", should never happen.";
return false;
}
}
static bool node_to_image_spec_atttributes(ImageSpec *image_spec,
const Node *node,
const string &attr_name_prefix)
{
for (const SocketType &socket : node->type->inputs) {
if (!node_socket_to_image_spec_atttributes(image_spec, node, socket, attr_name_prefix)) {
return false;
}
}
return true;
}
static bool node_from_image_spec_atttributes(Node *node,
const ImageSpec &image_spec,
const string &attr_name_prefix)
{
for (const SocketType &socket : node->type->inputs) {
if (!node_socket_from_image_spec_atttributes(node, socket, image_spec, attr_name_prefix)) {
return false;
}
}
return true;
}
static bool buffer_params_to_image_spec_atttributes(ImageSpec *image_spec,
const BufferParams &buffer_params)
{
if (!node_to_image_spec_atttributes(image_spec, &buffer_params, ATTR_BUFFER_SOCKET_PREFIX)) {
return false;
}
/* Passes storage is not covered by the node socket. so "expand" the loop manually. */
const int num_passes = buffer_params.passes.size();
image_spec->attribute(ATTR_PASSES_COUNT, num_passes);
for (int pass_index = 0; pass_index < num_passes; ++pass_index) {
const string attr_name_prefix = string_printf(ATTR_PASS_SOCKET_PREFIX_FORMAT, pass_index);
const BufferPass *pass = &buffer_params.passes[pass_index];
if (!node_to_image_spec_atttributes(image_spec, pass, attr_name_prefix)) {
return false;
}
}
return true;
}
static bool buffer_params_from_image_spec_atttributes(BufferParams *buffer_params,
const ImageSpec &image_spec)
{
if (!node_from_image_spec_atttributes(buffer_params, image_spec, ATTR_BUFFER_SOCKET_PREFIX)) {
return false;
}
/* Passes storage is not covered by the node socket. so "expand" the loop manually. */
const int num_passes = image_spec.get_int_attribute(ATTR_PASSES_COUNT, 0);
if (num_passes == 0) {
LOG_ERROR << "Missing passes count attribute.";
return false;
}
for (int pass_index = 0; pass_index < num_passes; ++pass_index) {
const string attr_name_prefix = string_printf(ATTR_PASS_SOCKET_PREFIX_FORMAT, pass_index);
BufferPass pass;
if (!node_from_image_spec_atttributes(&pass, image_spec, attr_name_prefix)) {
return false;
}
buffer_params->passes.emplace_back(std::move(pass));
}
buffer_params->update_passes();
return true;
}
/* Configure image specification for the given buffer parameters and passes.
*
* Image channels will be strictly ordered to match content of corresponding buffer, and the
* metadata will be set so that the render buffers and passes can be reconstructed from it.
*
* If the tile size different from (0, 0) the image specification will be configured to use the
* given tile size for tiled IO. */
static bool configure_image_spec_from_buffer(ImageSpec *image_spec,
const BufferParams &buffer_params,
const int2 tile_size = make_int2(0, 0))
{
const std::vector<std::string> channel_names = exr_channel_names_for_passes(buffer_params);
const int num_channels = channel_names.size();
*image_spec = ImageSpec(
buffer_params.width, buffer_params.height, num_channels, TypeDesc::FLOAT);
image_spec->channelnames = std::move(channel_names);
if (!buffer_params_to_image_spec_atttributes(image_spec, buffer_params)) {
return false;
}
if (tile_size.x != 0 || tile_size.y != 0) {
DCHECK_GT(tile_size.x, 0);
DCHECK_GT(tile_size.y, 0);
image_spec->tile_width = min(TileManager::IMAGE_TILE_SIZE, tile_size.x);
image_spec->tile_height = min(TileManager::IMAGE_TILE_SIZE, tile_size.y);
}
return true;
}
/* --------------------------------------------------------------------
* Tile Manager.
*/
TileManager::TileManager()
{
/* Use process ID to separate different processes.
* To ensure uniqueness from within a process use combination of object address and instance
* index. This solves problem of possible object re-allocation at the same time, and solves
* possible conflict when the counter overflows while there are still active instances of the
* class. */
const int tile_manager_id = g_instance_index.fetch_add(1, std::memory_order_relaxed);
tile_file_unique_part_ = to_string(system_self_process_id()) + "-" +
to_string(reinterpret_cast<uintptr_t>(this)) + "-" +
to_string(tile_manager_id);
}
TileManager::~TileManager() = default;
int TileManager::compute_render_tile_size(const int suggested_tile_size) const
{
/* Must be a multiple of IMAGE_TILE_SIZE so that we can write render tiles into the image file
* aligned on image tile boundaries. We can't set IMAGE_TILE_SIZE equal to the render tile size
* because too big tile size leads to integer overflow inside OpenEXR. */
const int computed_tile_size = (suggested_tile_size <= IMAGE_TILE_SIZE) ?
suggested_tile_size :
align_up(suggested_tile_size, IMAGE_TILE_SIZE);
return min(computed_tile_size, MAX_TILE_SIZE);
}
void TileManager::reset_scheduling(const BufferParams &params, const int2 tile_size)
{
LOG_DEBUG << "Using tile size of " << tile_size;
close_tile_output();
tile_size_ = tile_size;
tile_state_.num_tiles_x = tile_size_.x ? divide_up(params.width, tile_size_.x) : 0;
tile_state_.num_tiles_y = tile_size_.y ? divide_up(params.height, tile_size_.y) : 0;
tile_state_.num_tiles = tile_state_.num_tiles_x * tile_state_.num_tiles_y;
tile_state_.next_tile_index = 0;
tile_state_.current_tile = Tile();
}
void TileManager::update(const BufferParams &params, const Scene *scene)
{
DCHECK_NE(params.pass_stride, -1);
buffer_params_ = params;
if (has_multiple_tiles()) {
/* TODO(sergey): Proper Error handling, so that if configuration has failed we don't attempt to
* write to a partially configured file. */
configure_image_spec_from_buffer(&write_state_.image_spec, buffer_params_, tile_size_);
const DenoiseParams denoise_params = scene->integrator->get_denoise_params();
const AdaptiveSampling adaptive_sampling = scene->integrator->get_adaptive_sampling();
node_to_image_spec_atttributes(
&write_state_.image_spec, &denoise_params, ATTR_DENOISE_SOCKET_PREFIX);
/* Not adaptive sampling overscan yet for baking, would need overscan also
* for buffers read from the output driver. */
if (adaptive_sampling.use && !scene->bake_manager->get_baking()) {
overscan_ = 4;
}
else {
overscan_ = 0;
}
}
else {
write_state_.image_spec = ImageSpec();
overscan_ = 0;
}
}
void TileManager::set_temp_dir(const string &temp_dir)
{
temp_dir_ = temp_dir;
}
bool TileManager::done()
{
return tile_state_.next_tile_index == tile_state_.num_tiles;
}
bool TileManager::next()
{
if (done()) {
return false;
}
tile_state_.current_tile = get_tile_for_index(tile_state_.next_tile_index);
++tile_state_.next_tile_index;
return true;
}
Tile TileManager::get_tile_for_index(const int index) const
{
/* TODO(sergey): Consider using hilbert spiral, or. maybe, even configurable. Not sure this
* brings a lot of value since this is only applicable to BIG tiles. */
const int tile_index_y = index / tile_state_.num_tiles_x;
const int tile_index_x = index - tile_index_y * tile_state_.num_tiles_x;
const int tile_window_x = tile_index_x * tile_size_.x;
const int tile_window_y = tile_index_y * tile_size_.y;
Tile tile;
tile.x = max(0, tile_window_x - overscan_);
tile.y = max(0, tile_window_y - overscan_);
tile.window_x = tile_window_x - tile.x;
tile.window_y = tile_window_y - tile.y;
tile.window_width = min(tile_size_.x, buffer_params_.width - tile_window_x);
tile.window_height = min(tile_size_.y, buffer_params_.height - tile_window_y);
tile.width = min(buffer_params_.width - tile.x, tile.window_x + tile.window_width + overscan_);
tile.height = min(buffer_params_.height - tile.y,
tile.window_y + tile.window_height + overscan_);
return tile;
}
const Tile &TileManager::get_current_tile() const
{
return tile_state_.current_tile;
}
int2 TileManager::get_size() const
{
return make_int2(buffer_params_.width, buffer_params_.height);
}
bool TileManager::open_tile_output()
{
write_state_.filename = path_join(temp_dir_,
"cycles-tile-buffer-" + tile_file_unique_part_ + "-" +
to_string(write_state_.tile_file_index) + ".exr");
write_state_.tile_out = ImageOutput::create(write_state_.filename);
if (!write_state_.tile_out) {
LOG_ERROR << "Error creating image output for " << write_state_.filename;
return false;
}
if (!write_state_.tile_out->supports("tiles")) {
LOG_ERROR << "Progress tile file format does not support tiling.";
return false;
}
if (!write_state_.tile_out->open(write_state_.filename, write_state_.image_spec)) {
LOG_ERROR << "Error opening tile file: " << write_state_.tile_out->geterror();
write_state_.tile_out = nullptr;
return false;
}
write_state_.num_tiles_written = 0;
LOG_DEBUG << "Opened tile file " << write_state_.filename;
return true;
}
bool TileManager::close_tile_output()
{
if (!write_state_.tile_out) {
return true;
}
const bool success = write_state_.tile_out->close();
write_state_.tile_out = nullptr;
if (!success) {
LOG_ERROR << "Error closing tile file.";
return false;
}
LOG_DEBUG << "Tile output is closed.";
return true;
}
bool TileManager::write_tile(const RenderBuffers &tile_buffers)
{
if (!write_state_.tile_out) {
if (!open_tile_output()) {
return false;
}
}
const double time_start = time_dt();
DCHECK_EQ(tile_buffers.params.pass_stride, buffer_params_.pass_stride);
const BufferParams &tile_params = tile_buffers.params;
const int tile_x = tile_params.full_x - buffer_params_.full_x + tile_params.window_x;
const int tile_y = tile_params.full_y - buffer_params_.full_y + tile_params.window_y;
const int64_t pass_stride = tile_params.pass_stride;
const int64_t tile_row_stride = tile_params.width * pass_stride;
vector<float> pixel_storage;
const float *pixels = tile_buffers.buffer.data() + tile_params.window_x * pass_stride +
tile_params.window_y * tile_row_stride;
/* If there is an overscan used for the tile copy pixels into single continuous block of memory
* without any "gaps".
* This is a workaround for bug in OIIO (https://github.com/OpenImageIO/oiio/pull/3176).
* Our task reference: #93008. */
if (tile_params.window_x || tile_params.window_y ||
tile_params.window_width != tile_params.width ||
tile_params.window_height != tile_params.height)
{
pixel_storage.resize(pass_stride * tile_params.window_width * tile_params.window_height);
float *pixels_continuous = pixel_storage.data();
const int64_t pixels_row_stride = pass_stride * tile_params.width;
const int64_t pixels_continuous_row_stride = pass_stride * tile_params.window_width;
for (int i = 0; i < tile_params.window_height; ++i) {
memcpy(pixels_continuous, pixels, sizeof(float) * pixels_continuous_row_stride);
pixels += pixels_row_stride;
pixels_continuous += pixels_continuous_row_stride;
}
pixels = pixel_storage.data();
}
LOG_DEBUG << "Write tile at " << tile_x << ", " << tile_y;
/* The image tile sizes in the OpenEXR file are different from the size of our big tiles. The
* write_tiles() method expects a contiguous image region that will be split into tiles
* internally. OpenEXR expects the size of this region to be a multiple of the tile size,
* however OpenImageIO automatically adds the required padding.
*
* The only thing we have to ensure is that the tile_x and tile_y are a multiple of the
* image tile size, which happens in compute_render_tile_size. */
const int64_t xstride = pass_stride * sizeof(float);
const int64_t ystride = xstride * tile_params.window_width;
const int64_t zstride = ystride * tile_params.window_height;
if (!write_state_.tile_out->write_tiles(tile_x,
tile_x + tile_params.window_width,
tile_y,
tile_y + tile_params.window_height,
0,
1,
TypeDesc::FLOAT,
pixels,
xstride,
ystride,
zstride))
{
LOG_ERROR << "Error writing tile " << write_state_.tile_out->geterror();
return false;
}
++write_state_.num_tiles_written;
LOG_DEBUG << "Tile written in " << time_dt() - time_start << " seconds.";
return true;
}
void TileManager::finish_write_tiles()
{
if (!write_state_.tile_out) {
/* None of the tiles were written hence the file was not created.
* Avoid creation of fully empty file since it is redundant. */
return;
}
/* EXR expects all tiles to present in file. So explicitly write missing tiles as all-zero. */
if (write_state_.num_tiles_written < tile_state_.num_tiles) {
vector<float> pixel_storage(tile_size_.x * tile_size_.y * buffer_params_.pass_stride);
for (int tile_index = write_state_.num_tiles_written; tile_index < tile_state_.num_tiles;
++tile_index)
{
const Tile tile = get_tile_for_index(tile_index);
const int tile_x = tile.x + tile.window_x;
const int tile_y = tile.y + tile.window_y;
LOG_DEBUG << "Write dummy tile at " << tile_x << ", " << tile_y;
write_state_.tile_out->write_tiles(tile_x,
tile_x + tile.window_width,
tile_y,
tile_y + tile.window_height,
0,
1,
TypeDesc::FLOAT,
pixel_storage.data());
}
}
close_tile_output();
if (full_buffer_written_cb) {
full_buffer_written_cb(write_state_.filename);
}
LOG_DEBUG << "Tile file size is "
<< string_human_readable_number(path_file_size(write_state_.filename)) << " bytes.";
/* Advance the counter upon explicit finish of the file.
* Makes it possible to re-use tile manager for another scene, and avoids unnecessary increments
* of the tile-file-within-session index. */
++write_state_.tile_file_index;
write_state_.filename = "";
}
bool TileManager::read_full_buffer_from_disk(const string_view filename,
RenderBuffers *buffers,
DenoiseParams *denoise_params)
{
unique_ptr<ImageInput> in(ImageInput::open(filename));
if (!in) {
LOG_ERROR << "Error opening tile file " << filename;
return false;
}
const ImageSpec &image_spec = in->spec();
BufferParams buffer_params;
if (!buffer_params_from_image_spec_atttributes(&buffer_params, image_spec)) {
return false;
}
buffers->reset(buffer_params);
if (!node_from_image_spec_atttributes(denoise_params, image_spec, ATTR_DENOISE_SOCKET_PREFIX)) {
return false;
}
const int num_channels = in->spec().nchannels;
if (!in->read_image(0, 0, 0, num_channels, TypeDesc::FLOAT, buffers->buffer.data())) {
LOG_ERROR << "Error reading pixels from the tile file " << in->geterror();
return false;
}
if (!in->close()) {
LOG_ERROR << "Error closing tile file " << in->geterror();
return false;
}
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,181 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <functional>
#include "session/buffers.h"
#include "util/image.h"
#include "util/string.h"
#include "util/unique_ptr.h"
CCL_NAMESPACE_BEGIN
class DenoiseParams;
class Scene;
/* --------------------------------------------------------------------
* Tile.
*/
class Tile {
public:
int x = 0, y = 0;
int width = 0, height = 0;
int window_x = 0, window_y = 0;
int window_width = 0, window_height = 0;
Tile() = default;
};
/* --------------------------------------------------------------------
* Tile Manager.
*/
class TileManager {
public:
/* This callback is invoked by whenever on-dist tiles storage file is closed after writing. */
std::function<void(string_view)> full_buffer_written_cb;
TileManager();
~TileManager();
TileManager(const TileManager &other) = delete;
TileManager(TileManager &&other) noexcept = delete;
TileManager &operator=(const TileManager &other) = delete;
TileManager &operator=(TileManager &&other) = delete;
/* Reset current progress and start new rendering of the full-frame parameters in tiles of the
* given size.
* Only touches scheduling-related state of the tile manager. */
/* TODO(sergey): Consider using tile area instead of exact size to help dealing with extreme
* cases of stretched renders. */
void reset_scheduling(const BufferParams &params, const int2 tile_size);
/* Update for the known buffer passes and scene parameters.
* Will store all parameters needed for buffers access outside of the scene graph. */
void update(const BufferParams &params, const Scene *scene);
void set_temp_dir(const string &temp_dir);
int get_num_tiles() const
{
return tile_state_.num_tiles;
}
bool has_multiple_tiles() const
{
return tile_state_.num_tiles > 1;
}
int get_tile_overscan() const
{
return overscan_;
}
bool next();
bool done();
const Tile &get_current_tile() const;
int2 get_size() const;
/* Write render buffer of a tile to a file on disk.
*
* Opens file for write when first tile is written.
*
* Returns true on success. */
bool write_tile(const RenderBuffers &tile_buffers);
/* Inform the tile manager that no more tiles will be written to disk.
* The file will be considered final, all handles to it will be closed. */
void finish_write_tiles();
/* Check whether any tile has been written to disk. */
bool has_written_tiles() const
{
return write_state_.num_tiles_written != 0;
}
/* Read full frame render buffer from tiles file on disk.
*
* Returns true on success. */
bool read_full_buffer_from_disk(string_view filename,
RenderBuffers *buffers,
DenoiseParams *denoise_params);
/* Compute valid tile size compatible with image saving. */
int compute_render_tile_size(const int suggested_tile_size) const;
/* Tile size in the image file. */
static const int IMAGE_TILE_SIZE = 128;
/* Maximum supported tile size.
* Needs to be safe from allocation on a GPU point of view: the display driver needs to be able
* to allocate texture with the side size of this value.
* Use conservative value which is safe for most of OpenGL drivers and GPUs. */
static const int MAX_TILE_SIZE = 8192;
protected:
/* Get tile configuration for its index.
* The tile index must be within [0, state_.tile_state_). */
Tile get_tile_for_index(const int index) const;
bool open_tile_output();
bool close_tile_output();
string temp_dir_;
/* Part of an on-disk tile file name which avoids conflicts between several Cycles instances or
* several sessions. */
string tile_file_unique_part_;
int2 tile_size_ = make_int2(0, 0);
/* Number of extra pixels around the actual tile to render. */
int overscan_ = 0;
BufferParams buffer_params_;
/* Tile scheduling state. */
struct {
int num_tiles_x = 0;
int num_tiles_y = 0;
int num_tiles = 0;
int next_tile_index;
Tile current_tile;
} tile_state_;
/* State of tiles writing to a file on disk. */
struct {
/* Index of a tile file used during the current session.
* This number is used for the file name construction, making it possible to render several
* scenes throughout duration of the session and keep all results available for later read
* access. */
int tile_file_index = 0;
string filename;
/* Specification of the tile image which corresponds to the buffer parameters.
* Contains channels configured according to the passes configuration in the path traces.
*
* Output images are saved using this specification, input images are expected to have matched
* specification. */
ImageSpec image_spec;
/* Output handle for the tile file.
*
* This file can not be closed until all tiles has been provided, so the handle is stored in
* the state and is created whenever writing is requested. */
unique_ptr<ImageOutput> tile_out;
int num_tiles_written = 0;
} write_state_;
};
CCL_NAMESPACE_END