Add Chromium-only Blender WebEngine parity work
This commit is contained in:
42
blender-5.2.0/intern/cycles/session/CMakeLists.txt
Normal file
42
blender-5.2.0/intern/cycles/session/CMakeLists.txt
Normal 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})
|
||||
372
blender-5.2.0/intern/cycles/session/buffers.cpp
Normal file
372
blender-5.2.0/intern/cycles/session/buffers.cpp
Normal 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 ¶ms_)
|
||||
{
|
||||
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
|
||||
185
blender-5.2.0/intern/cycles/session/buffers.h
Normal file
185
blender-5.2.0/intern/cycles/session/buffers.h
Normal 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 ¶ms);
|
||||
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
|
||||
90
blender-5.2.0/intern/cycles/session/cache_eviction.cpp
Normal file
90
blender-5.2.0/intern/cycles/session/cache_eviction.cpp
Normal 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
|
||||
45
blender-5.2.0/intern/cycles/session/cache_eviction.h
Normal file
45
blender-5.2.0/intern/cycles/session/cache_eviction.h
Normal 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
|
||||
707
blender-5.2.0/intern/cycles/session/denoising.cpp
Normal file
707
blender-5.2.0/intern/cycles/session/denoising.cpp
Normal 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 ¶ms,
|
||||
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 ¶ms,
|
||||
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 ¶ms)
|
||||
{
|
||||
/* 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
|
||||
177
blender-5.2.0/intern/cycles/session/denoising.h
Normal file
177
blender-5.2.0/intern/cycles/session/denoising.h
Normal 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 ¶ms);
|
||||
~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 ¶ms,
|
||||
float *input_pixels);
|
||||
bool read_previous_pixels(const DenoiseImageLayer &layer,
|
||||
const BufferParams ¶ms,
|
||||
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
|
||||
86
blender-5.2.0/intern/cycles/session/display_driver.cpp
Normal file
86
blender-5.2.0/intern/cycles/session/display_driver.cpp
Normal 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
|
||||
194
blender-5.2.0/intern/cycles/session/display_driver.h
Normal file
194
blender-5.2.0/intern/cycles/session/display_driver.h
Normal 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 ¶ms, 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 ¶ms) = 0;
|
||||
};
|
||||
|
||||
CCL_NAMESPACE_END
|
||||
611
blender-5.2.0/intern/cycles/session/merge.cpp
Normal file
611
blender-5.2.0/intern/cycles/session/merge.cpp
Normal 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 ¤t_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
|
||||
28
blender-5.2.0/intern/cycles/session/merge.h
Normal file
28
blender-5.2.0/intern/cycles/session/merge.h
Normal 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
|
||||
70
blender-5.2.0/intern/cycles/session/output_driver.h
Normal file
70
blender-5.2.0/intern/cycles/session/output_driver.h
Normal 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
|
||||
815
blender-5.2.0/intern/cycles/session/session.cpp
Normal file
815
blender-5.2.0/intern/cycles/session/session.cpp
Normal 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 ¶ms_, 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
|
||||
259
blender-5.2.0/intern/cycles/session/session.h
Normal file
259
blender-5.2.0/intern/cycles/session/session.h
Normal 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 ¶ms) 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 ¶ms, 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
|
||||
664
blender-5.2.0/intern/cycles/session/tile.cpp
Normal file
664
blender-5.2.0/intern/cycles/session/tile.cpp
Normal 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 ¶ms, 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 ¶ms, 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
|
||||
181
blender-5.2.0/intern/cycles/session/tile.h
Normal file
181
blender-5.2.0/intern/cycles/session/tile.h
Normal 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 ¶ms, 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 ¶ms, 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
|
||||
Reference in New Issue
Block a user