Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,76 @@
# SPDX-FileCopyrightText: 2011-2022 Blender Foundation
#
# SPDX-License-Identifier: Apache-2.0
set(INC
..
)
set(INC_SYS
)
set(SRC
adaptive_sampling.cpp
denoiser.cpp
denoiser_gpu.cpp
denoiser_oidn.cpp
denoiser_oidn_base.cpp
denoiser_oidn_gpu.cpp
denoiser_optix.cpp
path_trace.cpp
tile.cpp
pass_accessor.cpp
pass_accessor_cpu.cpp
pass_accessor_gpu.cpp
path_trace_display.cpp
path_trace_tile.cpp
path_trace_work.cpp
path_trace_work_cpu.cpp
path_trace_work_gpu.cpp
render_scheduler.cpp
shader_eval.cpp
work_balancer.cpp
work_tile_scheduler.cpp
)
set(SRC_HEADERS
adaptive_sampling.h
denoiser.h
denoiser_gpu.h
denoiser_oidn.h
denoiser_oidn_base.h
denoiser_oidn_gpu.h
denoiser_optix.h
guiding.h
path_trace.h
tile.h
pass_accessor.h
pass_accessor_cpu.h
pass_accessor_gpu.h
path_trace_display.h
path_trace_tile.h
path_trace_work.h
path_trace_work_cpu.h
path_trace_work_gpu.h
render_scheduler.h
shader_eval.h
work_balancer.h
work_tile_scheduler.h
)
set(LIB
PUBLIC cycles_device
# NOTE: Is required for RenderBuffers access. Might consider moving files around a bit to
# avoid such cyclic dependency.
PUBLIC cycles_session
PUBLIC cycles_util
PRIVATE bf::dependencies::optional::openimagedenoise
PRIVATE bf::dependencies::optional::openpgl
)
include_directories(${INC})
include_directories(SYSTEM ${INC_SYS})
cycles_add_library(cycles_integrator "${LIB}" ${SRC} ${SRC_HEADERS})

View File

@@ -0,0 +1,57 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/adaptive_sampling.h"
#include "util/math.h"
CCL_NAMESPACE_BEGIN
AdaptiveSampling::AdaptiveSampling() = default;
int AdaptiveSampling::align_samples(const int start_sample, const int num_samples) const
{
if (!use) {
return num_samples;
}
/*
* The naive implementation goes as following:
*
* int count = 1;
* while (!need_filter(start_sample + count - 1) && count < num_samples) {
* ++count;
* }
* return count;
*/
/* 0-based sample index at which first filtering will happen. */
const int first_filter_sample = (min_samples + 1) | (adaptive_step - 1);
/* Allow as many samples as possible until the first filter sample. */
if (start_sample + num_samples <= first_filter_sample) {
return num_samples;
}
const int next_filter_sample = max(first_filter_sample, start_sample | (adaptive_step - 1));
const int num_samples_until_filter = next_filter_sample - start_sample + 1;
return min(num_samples_until_filter, num_samples);
}
bool AdaptiveSampling::need_filter(const int sample) const
{
if (!use) {
return false;
}
if (sample <= min_samples) {
return false;
}
return (sample & (adaptive_step - 1)) == (adaptive_step - 1);
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,43 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
CCL_NAMESPACE_BEGIN
class AdaptiveSampling {
public:
AdaptiveSampling();
/* Align number of samples so that they align with the adaptive filtering.
*
* Returns the new value for the `num_samples` so that after rendering so many samples on top
* of `start_sample` filtering is required.
*
* The alignment happens in a way that allows to render as many samples as possible without
* missing any filtering point. This means that the result is "clamped" by the nearest sample
* at which filtering is needed. This is part of mechanism which ensures that all devices will
* perform same exact filtering and adaptive sampling, regardless of their performance.
*
* `start_sample` is the 0-based index of sample.
*
* NOTE: The start sample is included into the number of samples to render. This means that
* if the number of samples is 1, then the path tracer will render samples [align_samples],
* if the number of samples is 2, then the path tracer will render samples [align_samples,
* align_samples + 1] and so on. */
int align_samples(const int start_sample, const int num_samples) const;
/* Check whether adaptive sampling filter should happen at this sample.
* Returns false if the adaptive sampling is not use.
*
* `sample` is the 0-based index of sample. */
bool need_filter(const int sample) const;
bool use = false;
int adaptive_step = 0;
int min_samples = 0;
float threshold = 0.0f;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,274 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/denoiser.h"
#include "device/device.h"
#include "integrator/denoiser_oidn.h"
#include "session/display_driver.h"
#ifdef WITH_OPENIMAGEDENOISE
# include "integrator/denoiser_oidn_gpu.h"
#endif
#ifdef WITH_OPTIX
# include "integrator/denoiser_optix.h"
#endif
#include "session/buffers.h"
#include "util/log.h"
#include "util/openimagedenoise.h"
#include "util/progress.h"
CCL_NAMESPACE_BEGIN
/* Check whether given device is single (not a MultiDevice). */
static bool is_single_device(const Device *device)
{
if (device->info.type == DEVICE_MULTI) {
/* Assume multi-device is never created with a single sub-device.
* If one requests such configuration it should be checked on the session level. */
return false;
}
if (!device->info.multi_devices.empty()) {
/* Some configurations will use multi_devices, but keep the type of an individual device.
* This does simplify checks for homogeneous setups, but here we really need a single device.
*/
return false;
}
return true;
}
/* Find best suitable device to perform denoiser on. Will iterate over possible sub-devices of
* multi-device. */
static Device *find_best_device(Device *device,
const DenoiserType type,
const GraphicsInteropDevice &interop_device)
{
Device *best_device = nullptr;
device->foreach_device([&](Device *sub_device) {
if ((sub_device->info.denoisers & type) == 0) {
return;
}
if (!best_device) {
best_device = sub_device;
}
else {
/* Prefer non-CPU devices over CPU for performance reasons. */
if (sub_device->info.type != DEVICE_CPU && best_device->info.type == DEVICE_CPU) {
best_device = sub_device;
}
/* Prefer a device that can use graphics interop for faster display update. */
if (sub_device->should_use_graphics_interop(interop_device) &&
!best_device->should_use_graphics_interop(interop_device))
{
best_device = sub_device;
}
/* TODO(sergey): Choose fastest device from available ones. Taking into account performance
* of the device and data transfer cost. */
}
});
return best_device;
}
bool use_optix_denoiser(Device *denoiser_device, const DenoiseParams &params)
{
#ifdef WITH_OPTIX
return (params.type == DENOISER_OPTIX &&
OptiXDenoiser::is_device_supported(denoiser_device->info));
#else
(void)denoiser_device;
(void)params;
return false;
#endif
}
bool use_gpu_oidn_denoiser(Device *denoiser_device, const DenoiseParams &params)
{
#ifdef WITH_OPENIMAGEDENOISE
return (params.type == DENOISER_OPENIMAGEDENOISE && params.use_gpu &&
OIDNDenoiserGPU::is_device_supported(denoiser_device->info));
#else
(void)denoiser_device;
(void)params;
return false;
#endif
}
DenoiseParams get_effective_denoise_params(Device *denoiser_device,
Device *cpu_fallback_device,
const DenoiseParams &params,
const GraphicsInteropDevice &interop_device,
Device *&single_denoiser_device)
{
DCHECK(params.use);
DenoiseParams effective_denoise_params = params;
single_denoiser_device = nullptr;
if (is_single_device(denoiser_device)) {
/* Simple case: denoising happens on a single device. */
single_denoiser_device = denoiser_device;
}
else {
/* Find best device from the ones which are proposed for denoising.
* The choice is expected to be between a few GPUs, or between a GPU and a CPU
* or between a few GPUs and a CPU. */
single_denoiser_device = find_best_device(denoiser_device, params.type, interop_device);
}
/* Ensure that we have a device to be used later in the code below. */
if (single_denoiser_device == nullptr) {
single_denoiser_device = cpu_fallback_device;
}
const bool is_cpu_denoiser_device = single_denoiser_device->info.type == DEVICE_CPU;
if (is_cpu_denoiser_device == false) {
if (use_optix_denoiser(single_denoiser_device, effective_denoise_params) ||
use_gpu_oidn_denoiser(single_denoiser_device, effective_denoise_params))
{
/* Denoising parameters are correct and there is no need to fall back to CPU OIDN. */
return effective_denoise_params;
}
}
/* Always fallback to OIDN on CPU. */
effective_denoise_params.type = DENOISER_OPENIMAGEDENOISE;
effective_denoise_params.use_gpu = false;
effective_denoise_params.upscale_factor = 1.0f;
return effective_denoise_params;
}
unique_ptr<Denoiser> Denoiser::create(Device *denoiser_device,
Device *cpu_fallback_device,
const DenoiseParams &params,
const GraphicsInteropDevice &interop_device)
{
Device *single_denoiser_device;
const DenoiseParams effective_denoiser_params = get_effective_denoise_params(
denoiser_device, cpu_fallback_device, params, interop_device, single_denoiser_device);
const bool is_cpu_denoiser_device = single_denoiser_device->info.type == DEVICE_CPU;
if (is_cpu_denoiser_device == false) {
#ifdef WITH_OPTIX
if (use_optix_denoiser(single_denoiser_device, effective_denoiser_params)) {
return make_unique<OptiXDenoiser>(single_denoiser_device, effective_denoiser_params);
}
#endif
#ifdef WITH_OPENIMAGEDENOISE
/* If available and allowed, then we will use OpenImageDenoise on GPU, otherwise on CPU. */
if (use_gpu_oidn_denoiser(single_denoiser_device, effective_denoiser_params)) {
return make_unique<OIDNDenoiserGPU>(single_denoiser_device, effective_denoiser_params);
}
#endif
}
if (!openimagedenoise_supported()) {
return nullptr;
}
/* Used preference CPU when possible, and fallback on CPU fallback device otherwise. */
return make_unique<OIDNDenoiser>(is_cpu_denoiser_device ? single_denoiser_device :
cpu_fallback_device,
effective_denoiser_params);
}
DenoiserType Denoiser::automatic_viewport_denoiser_type(const DeviceInfo &denoise_device_info)
{
#ifdef WITH_OPENIMAGEDENOISE
if (denoise_device_info.type != DEVICE_CPU &&
OIDNDenoiserGPU::is_device_supported(denoise_device_info))
{
return DENOISER_OPENIMAGEDENOISE;
}
#else
(void)denoise_device_info;
#endif
#ifdef WITH_OPTIX
if (OptiXDenoiser::is_device_supported(denoise_device_info)) {
return DENOISER_OPTIX;
}
#endif
#ifdef WITH_OPENIMAGEDENOISE
if (openimagedenoise_supported()) {
return DENOISER_OPENIMAGEDENOISE;
}
#endif
return DENOISER_NONE;
}
Denoiser::Denoiser(Device *denoiser_device, const DenoiseParams &params)
: denoiser_device_(denoiser_device), denoise_kernels_are_loaded_(false), params_(params)
{
DCHECK(denoiser_device_);
DCHECK(params.use);
}
void Denoiser::set_params(const DenoiseParams &params)
{
DCHECK_EQ(params.type, params_.type);
if (params.type == params_.type) {
params_ = params;
}
else {
LOG_ERROR << "Attempt to change denoiser type.";
}
}
const DenoiseParams &Denoiser::get_params() const
{
return params_;
}
bool Denoiser::load_kernels(Progress *progress)
{
/* If we have successfully loaded kernels once, then there is no need to repeat this again. */
if (denoise_kernels_are_loaded_) {
return denoise_kernels_are_loaded_;
}
if (progress) {
progress->set_status("Loading denoising kernels (may take a few minutes the first time)");
}
if (!denoiser_device_) {
set_error("No device available to denoise on");
return false;
}
/* Only need denoising feature, everything else is unused. */
if (!denoiser_device_->load_kernels(KERNEL_FEATURE_DENOISING)) {
string message = denoiser_device_->error_message();
if (message.empty()) {
message = "Failed loading denoising kernel, see console for errors";
}
set_error(message);
return false;
}
LOG_DEBUG << "Will denoise on " << denoiser_device_->info.description << " ("
<< denoiser_device_->info.id << ")";
denoise_kernels_are_loaded_ = true;
return true;
}
Device *Denoiser::get_denoiser_device() const
{
return denoiser_device_;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,134 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
/* TODO(sergey): The integrator folder might not be the best. Is easy to move files around if the
* better place is figured out. */
#include <functional>
#include "device/denoise.h"
#include "device/device.h"
#include "util/unique_ptr.h"
CCL_NAMESPACE_BEGIN
class BufferParams;
class Device;
class GraphicsInteropDevice;
class RenderBuffers;
class Progress;
bool use_optix_denoiser(Device *denoiser_device, const DenoiseParams &params);
bool use_gpu_oidn_denoiser(Device *denoiser_device, const DenoiseParams &params);
DenoiseParams get_effective_denoise_params(Device *denoiser_device,
Device *cpu_fallback_device,
const DenoiseParams &params,
const GraphicsInteropDevice &interop_device,
Device *&single_denoiser_device);
/* Implementation of a specific denoising algorithm.
*
* This class takes care of breaking down denoising algorithm into a series of device calls or to
* calls of an external API to denoise given input.
*
* TODO(sergey): Are we better with device or a queue here? */
class Denoiser {
public:
/* Create denoiser for the given path trace device.
*
* Notes:
* - The denoiser must be configured. This means that `params.use` must be true.
* This is checked in debug builds.
* - The device might be MultiDevice.
* - If Denoiser from params is not supported by provided denoise device, then Blender will
* fallback on the OIDN CPU denoising and use provided cpu_fallback_device.
* - Specifying the graphics interop device helps pick a more efficient denoising device.*/
static unique_ptr<Denoiser> create(Device *denoiser_device,
Device *cpu_fallback_device,
const DenoiseParams &params,
const GraphicsInteropDevice &interop_device);
virtual ~Denoiser() = default;
void set_params(const DenoiseParams &params);
const DenoiseParams &get_params() const;
/* Recommended type for viewport denoising. */
static DenoiserType automatic_viewport_denoiser_type(const DeviceInfo &denoise_device_info);
/* Create devices and load kernels needed for denoising.
* The progress is used to communicate state when kernels actually needs to be loaded.
*
* NOTE: The `progress` is an optional argument, can be nullptr. */
virtual bool load_kernels(Progress *progress);
/* Denoise the entire buffer.
*
* Buffer parameters denotes an effective parameters used during rendering. It could be
* a lower resolution render into a bigger allocated buffer, which is used in viewport during
* navigation and non-unit pixel size. Use that instead of render_buffers->params.
*
* The buffer might be coming from a "foreign" device from what this denoise is created for.
* This means that in general case the denoiser will make sure the input data is available on
* the denoiser device, perform denoising, and put data back to the device where the buffer
* came from.
*
* The `num_samples` corresponds to the number of samples in the render buffers. It is used
* to scale buffers down to the "final" value in algorithms which don't do automatic exposure,
* or which needs "final" value for data passes.
*
* The `allow_inplace_modification` means that the denoiser is allowed to do in-place
* modification of the input passes (scaling them down i.e.). This will lower the memory
* footprint of the denoiser but will make input passes "invalid" (from path tracer) point of
* view.
*
* Returns true when all passes are denoised. Will return false if there is a denoiser error (for
* example, caused by misconfigured denoiser) or when user requested to cancel rendering. */
virtual bool denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
int num_samples,
bool allow_inplace_modification,
float2 pixel_jitter = {}) = 0;
/* Get a device which is used to perform actual denoising.
*
* Notes:
*
* - The device can be different from the path tracing device. This happens, for example, when
* using OptiX denoiser and rendering on CPU.
*
* - No threading safety is ensured in this call. This means, that it is up to caller to ensure
* that there is no threading-conflict between denoising task lazily initializing the device
* and access to this device happen. */
Device *get_denoiser_device() const;
std::function<bool(void)> is_cancelled_cb;
bool is_cancelled() const
{
if (!is_cancelled_cb) {
return false;
}
return is_cancelled_cb();
}
void set_error(const string &error)
{
denoiser_device_->set_error(error);
}
protected:
Denoiser(Device *denoiser_device, const DenoiseParams &params);
Device *denoiser_device_;
bool denoise_kernels_are_loaded_;
DenoiseParams params_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,464 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/denoiser_gpu.h"
#include "device/denoise.h"
#include "device/device.h"
#include "device/memory.h"
#include "device/queue.h"
#include "integrator/pass_accessor_gpu.h"
#include "session/buffers.h"
#include "util/log.h"
CCL_NAMESPACE_BEGIN
DenoiserGPU::DenoiserGPU(Device *denoiser_device, const DenoiseParams &params)
: Denoiser(denoiser_device, params)
{
denoiser_queue_ = denoiser_device->gpu_queue_create();
DCHECK(denoiser_queue_);
}
DenoiserGPU::~DenoiserGPU() // NOLINT
{
/* Explicit implementation, to allow forward declaration of Device in the header. */
}
bool DenoiserGPU::denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
const int num_samples,
const bool allow_inplace_modification,
const float2 pixel_jitter)
{
Device *denoiser_device = get_denoiser_device();
if (!denoiser_device) {
return false;
}
RenderBuffers local_render_buffers(denoiser_device);
bool local_buffer_used = false;
if (denoiser_device == render_buffers->buffer.device) {
/* The device can access an existing buffer pointer. */
local_buffer_used = false;
}
else {
LOG_DEBUG << "Creating temporary buffer on denoiser device.";
/* Create buffer which is available by the device used by denoiser. */
/* TODO(sergey): Optimize data transfers. For example, only copy denoising related passes,
* ignoring other light ad data passes. */
local_buffer_used = true;
render_buffers->copy_from_device();
local_render_buffers.reset(denoised_buffer_params);
/* NOTE: The local buffer is allocated for an exact size of the effective render size, while
* the input render buffer is allocated for the lowest resolution divider possible. So it is
* important to only copy actually needed part of the input buffer. */
memcpy(local_render_buffers.buffer.data(),
render_buffers->buffer.data(),
sizeof(float) * local_render_buffers.buffer.size());
denoiser_queue_->copy_to_device(local_render_buffers.buffer);
}
{
DenoiseContext context(denoiser_device,
params_,
buffer_params,
denoised_buffer_params,
local_buffer_used ? &local_render_buffers : render_buffers,
num_samples,
local_buffer_used || allow_inplace_modification,
pixel_jitter);
if (!denoise_ensure(context)) {
return false;
}
if (!denoise_filter_guiding_preprocess(context)) {
LOG_ERROR << "Error preprocessing guiding passes.";
return false;
}
/* Passes which will use real albedo when it is available. */
if (!denoise_pass(context, PASS_COMBINED)) {
return false;
}
if (!denoise_pass(context, PASS_SHADOW_CATCHER_MATTE)) {
return false;
}
/* Passes which do not need albedo and hence if real is present it needs to become fake. */
if (!denoise_pass(context, PASS_SHADOW_CATCHER)) {
return false;
}
}
if (local_buffer_used) {
local_render_buffers.copy_from_device();
render_buffers_host_copy_denoised(render_buffers,
denoised_buffer_params,
&local_render_buffers,
local_render_buffers.params);
render_buffers->copy_to_device();
}
return true;
}
bool DenoiserGPU::denoise_ensure(DenoiseContext &context)
{
if (!denoise_create_if_needed(context)) {
LOG_ERROR << "GPU denoiser creation has failed.";
return false;
}
if (!denoise_configure_if_needed(context)) {
LOG_ERROR << "GPU denoiser configuration has failed.";
return false;
}
return true;
}
bool DenoiserGPU::denoise_filter_guiding_preprocess(const DenoiseContext &context)
{
const BufferParams &buffer_params = context.buffer_params;
const int work_size = buffer_params.width * buffer_params.height;
const DeviceKernelArguments args(&context.guiding_params.device_pointer,
&context.guiding_params.pass_stride,
&context.guiding_params.pass_albedo,
&context.guiding_params.pass_normal,
&context.guiding_params.pass_flow,
&context.render_buffers->buffer.device_pointer,
&buffer_params.offset,
&buffer_params.stride,
&buffer_params.pass_stride,
&context.pass_sample_count,
&context.pass_denoising_albedo,
&context.pass_denoising_normal,
&context.pass_motion,
&buffer_params.full_x,
&buffer_params.full_y,
&buffer_params.width,
&buffer_params.height,
&context.num_samples);
return denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_GUIDING_PREPROCESS, work_size, args) &&
denoise_filter_guiding_flip_y(context);
}
DenoiserGPU::DenoiseContext::DenoiseContext(Device *device,
const DenoiseParams &params,
const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
const int num_samples,
const bool allow_inplace_modification,
const float2 pixel_jitter)
: denoise_params(params),
render_buffers(render_buffers),
buffer_params(buffer_params),
denoised_buffer_params(denoised_buffer_params),
guiding_buffer(device, "denoiser guiding passes buffer", true),
use_guiding_passes(params.passes != DENOISER_PASS_NONE),
num_samples(num_samples),
pixel_jitter(pixel_jitter)
{
pass_motion = buffer_params.get_pass_offset(PASS_MOTION);
pass_sample_count = buffer_params.get_pass_offset(PASS_SAMPLE_COUNT);
if (params.passes & DENOISER_PASS_ALBEDO) {
pass_denoising_albedo = buffer_params.get_pass_offset(PASS_DENOISING_ALBEDO);
}
if (params.passes & DENOISER_PASS_NORMAL) {
pass_denoising_normal = buffer_params.get_pass_offset(PASS_DENOISING_NORMAL);
}
if (params.temporally_stable) {
prev_output.device_pointer = render_buffers->buffer.device_pointer;
prev_output.offset = buffer_params.get_pass_offset(PASS_DENOISING_PREVIOUS);
prev_output.stride = buffer_params.stride;
prev_output.pass_stride = buffer_params.pass_stride;
}
if (use_guiding_passes) {
if (allow_inplace_modification) {
guiding_params.device_pointer = render_buffers->buffer.device_pointer;
guiding_params.pass_albedo = pass_denoising_albedo;
guiding_params.pass_normal = pass_denoising_normal;
guiding_params.pass_flow = pass_motion;
guiding_params.stride = buffer_params.stride;
guiding_params.pass_stride = buffer_params.pass_stride;
}
else {
guiding_params.pass_stride = 0;
if (params.passes & DENOISER_PASS_ALBEDO) {
guiding_params.pass_albedo = guiding_params.pass_stride;
guiding_params.pass_stride += 3;
}
if (params.passes & DENOISER_PASS_NORMAL) {
guiding_params.pass_normal = guiding_params.pass_stride;
guiding_params.pass_stride += 3;
}
if (params.passes & DENOISER_PASS_MOTION) {
guiding_params.pass_flow = guiding_params.pass_stride;
guiding_params.pass_stride += 2;
}
guiding_params.stride = buffer_params.width;
guiding_buffer.alloc_to_device(buffer_params.width * buffer_params.height *
guiding_params.pass_stride);
guiding_params.device_pointer = guiding_buffer.device_pointer;
}
}
}
bool DenoiserGPU::denoise_filter_color_postprocess(const DenoiseContext &context,
const DenoisePass &pass)
{
if (!denoise_filter_color_flip_y(context, context.denoised_buffer_params, pass)) {
return false;
}
const BufferParams &buffer_params = context.denoised_buffer_params;
const int work_size = buffer_params.width * buffer_params.height;
const DeviceKernelArguments args(&context.render_buffers->buffer.device_pointer,
&buffer_params.full_x,
&buffer_params.full_y,
&buffer_params.width,
&buffer_params.height,
&buffer_params.offset,
&buffer_params.stride,
&context.buffer_params.full_x,
&context.buffer_params.full_y,
&context.buffer_params.offset,
&context.buffer_params.stride,
&buffer_params.pass_stride,
&context.num_samples,
&pass.noisy_offset,
&pass.denoised_offset,
&context.pass_sample_count,
&pass.num_components,
&pass.use_compositing,
&params_.upscale_factor);
return denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_COLOR_POSTPROCESS, work_size, args);
}
bool DenoiserGPU::denoise_filter_color_preprocess(const DenoiseContext &context,
const DenoisePass &pass)
{
if (context.denoise_params.type != DENOISER_OPTIX) {
/* Pass preprocessing is used to clamp values for the OptiX denoiser.
* Clamping is not necessary for other denoisers, so just skip this preprocess step. */
return true;
}
if (!denoise_filter_color_flip_y(context, context.buffer_params, pass)) {
return false;
}
const BufferParams &buffer_params = context.buffer_params;
const int work_size = buffer_params.width * buffer_params.height;
const DeviceKernelArguments args(&context.render_buffers->buffer.device_pointer,
&buffer_params.full_x,
&buffer_params.full_y,
&buffer_params.width,
&buffer_params.height,
&buffer_params.offset,
&buffer_params.stride,
&buffer_params.pass_stride,
&pass.denoised_offset);
return denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_COLOR_PREPROCESS, work_size, args);
}
bool DenoiserGPU::denoise_filter_color_flip_y(const DenoiseContext &context,
const BufferParams &buffer_params,
const DenoisePass &pass)
{
if (context.denoise_params.type != DENOISER_OPTIX || context.denoise_params.temporally_stable) {
/* Flipping the image is used to improve result quality with the OptiX denoiser.
* It is not necessary for other denoisers, so just skip this preprocess step. */
return true;
}
const int work_size = buffer_params.width * buffer_params.height / 2;
const DeviceKernelArguments args(&context.render_buffers->buffer.device_pointer,
&buffer_params.full_x,
&buffer_params.full_y,
&buffer_params.width,
&buffer_params.height,
&buffer_params.offset,
&buffer_params.stride,
&buffer_params.pass_stride,
&pass.denoised_offset);
return denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_COLOR_FLIP_Y, work_size, args);
}
bool DenoiserGPU::denoise_filter_guiding_flip_y(const DenoiseContext &context)
{
if (context.denoise_params.type != DENOISER_OPTIX || context.denoise_params.temporally_stable) {
/* Flipping the image is used to improve result quality with the OptiX denoiser.
* It is not necessary for other denoisers, so just skip this preprocess step. */
return true;
}
const BufferParams &buffer_params = context.buffer_params;
const int guiding_offset = 0;
const int work_size = buffer_params.width * buffer_params.height / 2;
const int guiding_passes[] = {context.guiding_params.pass_albedo,
context.guiding_params.pass_normal};
for (const int guiding_pass : guiding_passes) {
if (guiding_pass == PASS_UNUSED) {
continue;
}
const DeviceKernelArguments args(&context.guiding_params.device_pointer,
&guiding_offset,
&guiding_offset,
&buffer_params.width,
&buffer_params.height,
&guiding_offset,
&context.guiding_params.stride,
&context.guiding_params.pass_stride,
&guiding_pass);
if (!denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_COLOR_FLIP_Y, work_size, args)) {
return false;
}
}
return true;
}
bool DenoiserGPU::denoise_filter_guiding_set_fake_albedo(const DenoiseContext &context)
{
const BufferParams &buffer_params = context.buffer_params;
const int work_size = buffer_params.width * buffer_params.height;
const DeviceKernelArguments args(&context.guiding_params.device_pointer,
&context.guiding_params.pass_stride,
&context.guiding_params.pass_albedo,
&buffer_params.width,
&buffer_params.height);
return denoiser_queue_->enqueue(DEVICE_KERNEL_FILTER_GUIDING_SET_FAKE_ALBEDO, work_size, args);
}
void DenoiserGPU::denoise_color_read(const DenoiseContext &context, const DenoisePass &pass)
{
PassAccessor::PassAccessInfo pass_access_info;
pass_access_info.type = pass.type;
pass_access_info.mode = PassMode::NOISY;
pass_access_info.offset = pass.noisy_offset;
/* Denoiser operates on passes which are used to calculate the approximation, and is never used
* on the approximation. The latter is not even possible because OptiX does not support
* denoising of semi-transparent pixels. */
pass_access_info.use_approximate_shadow_catcher = false;
pass_access_info.use_approximate_shadow_catcher_background = false;
pass_access_info.show_active_pixels = false;
/* TODO(sergey): Consider adding support of actual exposure, to avoid clamping in extreme cases.
*/
const PassAccessorGPU pass_accessor(
denoiser_queue_.get(), pass_access_info, 1.0f, context.num_samples);
PassAccessor::Destination destination(pass_access_info.type, pass_access_info.mode);
destination.d_pixels = context.render_buffers->buffer.device_pointer;
destination.num_components = 3;
destination.pixel_offset = pass.denoised_offset;
destination.pixel_stride = context.buffer_params.pass_stride;
BufferParams buffer_params = context.buffer_params;
buffer_params.window_x = 0;
buffer_params.window_y = 0;
buffer_params.window_width = buffer_params.width;
buffer_params.window_height = buffer_params.height;
pass_accessor.get_render_tile_pixels(context.render_buffers, buffer_params, destination);
}
bool DenoiserGPU::denoise_pass(DenoiseContext &context, PassType pass_type)
{
const BufferParams &buffer_params = context.buffer_params;
const DenoisePass pass(pass_type, buffer_params);
if (pass.noisy_offset == PASS_UNUSED) {
return true;
}
if (pass.denoised_offset == PASS_UNUSED) {
LOG_DFATAL << "Missing denoised pass " << pass_type_as_string(pass_type);
return false;
}
if (pass.use_denoising_albedo) {
if (context.albedo_replaced_with_fake) {
LOG_ERROR << "Pass which requires albedo is denoised after fake albedo has been set.";
return false;
}
}
else if (context.use_guiding_passes && !context.albedo_replaced_with_fake) {
context.albedo_replaced_with_fake = true;
if (!denoise_filter_guiding_set_fake_albedo(context)) {
LOG_ERROR << "Error replacing real albedo with the fake one.";
return false;
}
}
/* Read and preprocess noisy color input pass. */
denoise_color_read(context, pass);
if (!denoise_filter_color_preprocess(context, pass)) {
LOG_ERROR << "Error converting denoising passes to RGB buffer.";
return false;
}
if (!denoise_run(context, pass)) {
LOG_ERROR << "Error running denoiser.";
return false;
}
/* Store result in the combined pass of the render buffer.
*
* This will scale the denoiser result up to match the number of, possibly per-pixel, samples. */
if (!denoise_filter_color_postprocess(context, pass)) {
LOG_ERROR << "Error copying denoiser result to the denoised pass.";
return false;
}
return denoiser_queue_->synchronize();
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/denoiser.h"
#include "session/buffers.h"
CCL_NAMESPACE_BEGIN
/* Implementation of Denoiser which uses a device-specific denoising implementation, running on a
* GPU device queue. It makes sure the to-be-denoised buffer is available on the denoising device
* and invokes denoising kernels via the device queue API. */
class DenoiserGPU : public Denoiser {
public:
DenoiserGPU(Device *denoiser_device, const DenoiseParams &params);
~DenoiserGPU() override;
bool denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
int num_samples,
bool allow_inplace_modification,
float2 pixel_jitter) override;
protected:
class DenoisePass;
class DenoiseContext;
/* Make sure the GPU denoiser is created and configured. */
virtual bool denoise_ensure(DenoiseContext &context);
/* Create GPU denoiser descriptor if needed.
* Will do nothing if the current GPU descriptor is usable for the given parameters.
* If the GPU denoiser descriptor did re-allocate here it is left unconfigured. */
virtual bool denoise_create_if_needed(DenoiseContext &context) = 0;
/* Configure existing GPU denoiser descriptor for the use for the given task. */
virtual bool denoise_configure_if_needed(DenoiseContext &context) = 0;
/* Read input color pass from the render buffer into the memory which corresponds to the noisy
* input within the given context. Pixels are scaled to the number of samples, but are not
* preprocessed yet. */
void denoise_color_read(const DenoiseContext &context, const DenoisePass &pass);
/* Run corresponding filter kernels, preparing data for the denoiser or copying data from the
* denoiser result to the render buffer. */
bool denoise_filter_color_preprocess(const DenoiseContext &context, const DenoisePass &pass);
bool denoise_filter_color_postprocess(const DenoiseContext &context, const DenoisePass &pass);
bool denoise_filter_color_flip_y(const DenoiseContext &context,
const BufferParams &buffer_params,
const DenoisePass &pass);
bool denoise_filter_guiding_flip_y(const DenoiseContext &context);
bool denoise_filter_guiding_set_fake_albedo(const DenoiseContext &context);
/* Read guiding passes from the render buffers, preprocess them in a way which is expected by
* the GPU denoiser and store in the guiding passes memory within the given context.
*
* Pre-processing of the guiding passes is to only happen once per context lifetime. DO not
* preprocess them for every pass which is being denoised. */
bool denoise_filter_guiding_preprocess(const DenoiseContext &context);
bool denoise_pass(DenoiseContext &context, PassType pass_type);
/* Returns true if task is fully handled. */
virtual bool denoise_run(const DenoiseContext &context, const DenoisePass &pass) = 0;
unique_ptr<DeviceQueue> denoiser_queue_;
class DenoisePass {
public:
DenoisePass(const PassType type, const BufferParams &buffer_params) : type(type)
{
noisy_offset = buffer_params.get_pass_offset(type, PassMode::NOISY);
denoised_offset = buffer_params.get_pass_offset(type, PassMode::DENOISED);
const PassInfo pass_info = Pass::get_info(type);
num_components = pass_info.num_components;
use_compositing = pass_info.use_compositing;
use_denoising_albedo = pass_info.use_denoising_albedo;
}
PassType type;
int noisy_offset;
int denoised_offset;
int num_components;
int use_compositing;
bool use_denoising_albedo;
};
class DenoiseContext {
public:
explicit DenoiseContext(Device *device,
const DenoiseParams &params,
const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
int num_samples,
bool allow_inplace_modification,
float2 pixel_jitter);
const DenoiseParams &denoise_params;
RenderBuffers *render_buffers = nullptr;
const BufferParams &buffer_params;
const BufferParams &denoised_buffer_params;
/* Previous output. */
struct {
device_ptr device_pointer = 0;
int offset = PASS_UNUSED;
int stride = -1;
int pass_stride = -1;
} prev_output;
/* Device-side storage of the guiding passes. */
device_only_memory<float> guiding_buffer;
struct {
device_ptr device_pointer = 0;
/* NOTE: Are only initialized when the corresponding guiding pass is enabled. */
int pass_albedo = PASS_UNUSED;
int pass_normal = PASS_UNUSED;
int pass_flow = PASS_UNUSED;
int stride = -1;
int pass_stride = -1;
} guiding_params;
const bool use_guiding_passes = false;
int num_samples = 0;
int pass_sample_count = PASS_UNUSED;
/* NOTE: Are only initialized when the corresponding guiding pass is enabled. */
int pass_denoising_albedo = PASS_UNUSED;
int pass_denoising_normal = PASS_UNUSED;
int pass_motion = PASS_UNUSED;
/* For passes which don't need albedo channel for denoising we replace the actual albedo with
* the (0.5, 0.5, 0.5). This flag indicates that the real albedo pass has been replaced with
* the fake values and denoising of passes which do need albedo can no longer happen. */
bool albedo_replaced_with_fake = false;
/* Sub-pixel jitter offset of the current frame. This can be used for upscaling. */
float2 pixel_jitter;
};
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,744 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/denoiser_oidn.h"
#include <array>
#include "device/device.h"
#include "device/queue.h"
#include "integrator/pass_accessor_cpu.h"
#include "session/buffers.h"
#include "util/array.h"
#include "util/log.h"
#include "util/openimagedenoise.h"
#include "util/path.h"
CCL_NAMESPACE_BEGIN
thread_mutex OIDNDenoiser::mutex_;
OIDNDenoiser::OIDNDenoiser(Device *denoiser_device, const DenoiseParams &params)
: Denoiser(denoiser_device, params)
#ifdef WITH_OPENIMAGEDENOISE
,
base_(this)
#endif
{
DCHECK_EQ(params.type, DENOISER_OPENIMAGEDENOISE);
#ifndef WITH_OPENIMAGEDENOISE
set_error("Failed to denoise, build has no OpenImageDenoise support");
#else
if (!openimagedenoise_supported()) {
set_error("OpenImageDenoiser is not supported on this CPU: missing SSE 4.1 support");
}
#endif
}
#ifdef WITH_OPENIMAGEDENOISE
static bool oidn_progress_monitor_function(void *user_ptr, double /*n*/)
{
OIDNDenoiser *oidn_denoiser = reinterpret_cast<OIDNDenoiser *>(user_ptr);
return !oidn_denoiser->is_cancelled();
}
class OIDNPass {
public:
OIDNPass() = default;
OIDNPass(const BufferParams &buffer_params,
const char *name,
PassType type,
PassMode mode = PassMode::NOISY)
: name(name), type(type), mode(mode)
{
offset = buffer_params.get_pass_offset(type, mode);
need_scale = (type == PASS_DENOISING_ALBEDO || type == PASS_DENOISING_NORMAL);
const PassInfo pass_info = Pass::get_info(type, mode);
num_components = pass_info.num_components;
use_compositing = pass_info.use_compositing;
use_denoising_albedo = pass_info.use_denoising_albedo;
}
operator bool() const
{
return name[0] != '\0';
}
/* Name of an image which will be passed to the OIDN library.
* Should be one of the following: color, albedo, normal, output.
* The albedo and normal images are optional. */
const char *name = "";
PassType type = PASS_NONE;
PassMode mode = PassMode::NOISY;
int num_components = -1;
bool use_compositing = false;
bool use_denoising_albedo = true;
/* Offset of beginning of this pass in the render buffers. */
int offset = -1;
/* Denotes whether the data is to be scaled down with the number of passes.
* Is required for albedo and normal passes. The color pass OIDN will perform auto-exposure, so
* scaling is not needed for the color pass unless adaptive sampling is used.
*
* NOTE: Do not scale the output pass, as that requires to be a pointer in the original buffer.
* All the scaling on the output needed for integration with adaptive sampling will happen
* outside of generic pass handling. */
bool need_scale = false;
/* The content of the pass has been pre-filtered. */
bool is_filtered = false;
/* For the scaled passes, the data which holds values of scaled pixels. */
array<float> scaled_buffer;
};
class OIDNDenoiseContext {
friend class OIDNDenoiser;
public:
OIDNDenoiseContext(OIDNDenoiser *denoiser,
const DenoiseParams &denoise_params,
const BufferParams &buffer_params,
RenderBuffers *render_buffers,
const int num_samples,
const bool allow_inplace_modification)
: denoiser_(denoiser),
denoise_params_(denoise_params),
buffer_params_(buffer_params),
render_buffers_(render_buffers),
num_samples_(num_samples),
allow_inplace_modification_(allow_inplace_modification),
pass_sample_count_(buffer_params_.get_pass_offset(PASS_SAMPLE_COUNT))
{
if (denoise_params_.passes & DENOISER_PASS_ALBEDO) {
oidn_albedo_pass_ = OIDNPass(buffer_params_, "albedo", PASS_DENOISING_ALBEDO);
}
if (denoise_params_.passes & DENOISER_PASS_NORMAL) {
oidn_normal_pass_ = OIDNPass(buffer_params_, "normal", PASS_DENOISING_NORMAL);
}
}
bool need_denoising() const
{
if (buffer_params_.width == 0 && buffer_params_.height == 0) {
return false;
}
return true;
}
/* Make the guiding passes available by a sequential denoising of various passes. */
void read_guiding_passes()
{
read_guiding_pass(oidn_albedo_pass_);
read_guiding_pass(oidn_normal_pass_);
}
bool denoise_pass(const PassType pass_type)
{
OIDNPass oidn_color_pass(buffer_params_, "color", pass_type);
if (oidn_color_pass.offset == PASS_UNUSED) {
return true;
}
if (oidn_color_pass.use_denoising_albedo) {
if (albedo_replaced_with_fake_) {
LOG_ERROR << "Pass which requires albedo is denoised after fake albedo has been set.";
return false;
}
}
OIDNPass oidn_output_pass(buffer_params_, "output", pass_type, PassMode::DENOISED);
if (oidn_output_pass.offset == PASS_UNUSED) {
LOG_DFATAL << "Missing denoised pass " << pass_type_as_string(pass_type);
return false;
}
OIDNPass oidn_color_access_pass = read_input_pass(oidn_color_pass, oidn_output_pass);
if (!denoiser_->base_.oidn_filter_) {
denoiser_->set_error("OpenImageDenoise filter is not initialized");
return false;
}
if (!filter_guiding_pass_if_needed(oidn_albedo_pass_)) {
return false;
}
if (!filter_guiding_pass_if_needed(oidn_normal_pass_)) {
return false;
}
set_input_pass(denoiser_->base_.oidn_filter_, oidn_color_access_pass);
set_guiding_passes(denoiser_->base_.oidn_filter_, oidn_color_pass);
set_output_pass(denoiser_->base_.oidn_filter_, oidn_output_pass);
const bool clean_aux = denoise_params_.prefilter != DENOISER_PREFILTER_FAST;
oidnSetFilterInt(denoiser_->base_.oidn_filter_, "cleanAux", clean_aux);
if (!denoiser_->commit_and_execute_filter(denoiser_->base_.oidn_filter_)) {
return false;
}
postprocess_output(oidn_color_pass, oidn_output_pass);
return true;
}
protected:
bool filter_guiding_pass_if_needed(OIDNPass &oidn_pass)
{
if (denoise_params_.prefilter != DENOISER_PREFILTER_ACCURATE || !oidn_pass ||
oidn_pass.is_filtered)
{
return true;
}
OIDNFilter filter = (oidn_pass.type == PASS_DENOISING_ALBEDO) ?
denoiser_->base_.albedo_filter_ :
denoiser_->base_.normal_filter_;
if (!filter) {
denoiser_->set_error("OpenImageDenoise guiding filter is not initialized");
return false;
}
set_pass(filter, oidn_pass);
set_output_pass(filter, oidn_pass);
if (!denoiser_->commit_and_execute_filter(filter)) {
return false;
}
oidn_pass.is_filtered = true;
return true;
}
/* Make pixels of a guiding pass available by the denoiser. */
void read_guiding_pass(OIDNPass &oidn_pass)
{
if (!oidn_pass) {
return;
}
DCHECK(!oidn_pass.use_compositing);
if (denoise_params_.prefilter != DENOISER_PREFILTER_ACCURATE &&
!is_pass_scale_needed(oidn_pass))
{
/* Pass data is available as-is from the render buffers. */
return;
}
if (allow_inplace_modification_) {
scale_pass_in_render_buffers(oidn_pass);
return;
}
read_pass_pixels_into_buffer(oidn_pass);
}
/* Special reader of the input pass.
* To save memory it will read pixels into the output, and let the denoiser to perform an
* in-place operation. */
OIDNPass read_input_pass(OIDNPass &oidn_input_pass, const OIDNPass &oidn_output_pass)
{
const bool use_compositing = oidn_input_pass.use_compositing;
/* Simple case: no compositing is involved, no scaling is needed.
* The pass pixels will be referenced as-is, without extra processing. */
if (!use_compositing && !is_pass_scale_needed(oidn_input_pass)) {
return oidn_input_pass;
}
float *buffer_data = render_buffers_->buffer.data();
float *pass_data = buffer_data + oidn_output_pass.offset;
PassAccessor::Destination destination(pass_data, 3);
destination.pixel_stride = buffer_params_.pass_stride;
read_pass_pixels(oidn_input_pass, destination);
OIDNPass oidn_input_pass_at_output = oidn_input_pass;
oidn_input_pass_at_output.offset = oidn_output_pass.offset;
return oidn_input_pass_at_output;
}
/* Read pass pixels using PassAccessor into the given destination. */
void read_pass_pixels(const OIDNPass &oidn_pass, const PassAccessor::Destination &destination)
{
PassAccessor::PassAccessInfo pass_access_info;
pass_access_info.type = oidn_pass.type;
pass_access_info.mode = oidn_pass.mode;
pass_access_info.offset = oidn_pass.offset;
/* Denoiser operates on passes which are used to calculate the approximation, and is never used
* on the approximation. The latter is not even possible because OIDN does not support
* denoising of semi-transparent pixels. */
pass_access_info.use_approximate_shadow_catcher = false;
pass_access_info.use_approximate_shadow_catcher_background = false;
pass_access_info.show_active_pixels = false;
/* OIDN will perform an auto-exposure, so it is not required to know exact exposure configured
* by users. What is important is to use same exposure for read and write access of the pass
* pixels. */
const PassAccessorCPU pass_accessor(pass_access_info, 1.0f, num_samples_);
BufferParams buffer_params = buffer_params_;
buffer_params.window_x = 0;
buffer_params.window_y = 0;
buffer_params.window_width = buffer_params.width;
buffer_params.window_height = buffer_params.height;
pass_accessor.get_render_tile_pixels(render_buffers_, buffer_params, destination);
}
/* Read pass pixels using PassAccessor into a temporary buffer which is owned by the pass.. */
void read_pass_pixels_into_buffer(OIDNPass &oidn_pass)
{
LOG_DEBUG << "Allocating temporary buffer for pass " << oidn_pass.name << " ("
<< pass_type_as_string(oidn_pass.type) << ")";
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
array<float> &scaled_buffer = oidn_pass.scaled_buffer;
scaled_buffer.resize(width * height * 3);
const PassAccessor::Destination destination(scaled_buffer.data(), 3);
read_pass_pixels(oidn_pass, destination);
}
/* Set OIDN image to reference pixels from the given render buffer pass.
* No transform to the pixels is done, no additional memory is used. */
void set_pass_referenced(OIDNFilter oidn_filter, const char *name, const OIDNPass &oidn_pass)
{
const int64_t x = buffer_params_.full_x;
const int64_t y = buffer_params_.full_y;
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
const int64_t offset = buffer_params_.offset;
const int64_t stride = buffer_params_.stride;
const int64_t pass_stride = buffer_params_.pass_stride;
const int64_t pixel_index = offset + x + y * stride;
const int64_t buffer_offset = pixel_index * pass_stride;
float *buffer_data = render_buffers_->buffer.data();
oidnSetSharedFilterImage(oidn_filter,
name,
buffer_data + buffer_offset + oidn_pass.offset,
OIDN_FORMAT_FLOAT3,
width,
height,
0,
pass_stride * sizeof(float),
stride * pass_stride * sizeof(float));
}
void set_pass_from_buffer(OIDNFilter oidn_filter, const char *name, OIDNPass &oidn_pass)
{
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
oidnSetSharedFilterImage(oidn_filter,
name,
oidn_pass.scaled_buffer.data(),
OIDN_FORMAT_FLOAT3,
width,
height,
0,
0,
0);
}
void set_pass(OIDNFilter oidn_filter, OIDNPass &oidn_pass)
{
set_pass(oidn_filter, oidn_pass.name, oidn_pass);
}
void set_pass(OIDNFilter oidn_filter, const char *name, OIDNPass &oidn_pass)
{
if (oidn_pass.scaled_buffer.empty()) {
set_pass_referenced(oidn_filter, name, oidn_pass);
}
else {
set_pass_from_buffer(oidn_filter, name, oidn_pass);
}
}
void set_input_pass(OIDNFilter oidn_filter, OIDNPass &oidn_pass)
{
set_pass_referenced(oidn_filter, oidn_pass.name, oidn_pass);
}
void set_guiding_passes(OIDNFilter oidn_filter, OIDNPass &oidn_pass)
{
if (oidn_albedo_pass_) {
if (oidn_pass.use_denoising_albedo) {
set_pass(oidn_filter, oidn_albedo_pass_);
}
else {
/* NOTE: OpenImageDenoise library implicitly expects albedo pass when normal pass has been
* provided. */
set_fake_albedo_pass(oidn_filter);
}
}
if (oidn_normal_pass_) {
set_pass(oidn_filter, oidn_normal_pass_);
}
}
void set_fake_albedo_pass(OIDNFilter oidn_filter)
{
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
if (!albedo_replaced_with_fake_) {
const int64_t num_pixel_components = width * height * 3;
oidn_albedo_pass_.scaled_buffer.resize(num_pixel_components);
for (int i = 0; i < num_pixel_components; ++i) {
oidn_albedo_pass_.scaled_buffer[i] = 0.5f;
}
albedo_replaced_with_fake_ = true;
}
set_pass(oidn_filter, oidn_albedo_pass_);
}
void set_output_pass(OIDNFilter oidn_filter, OIDNPass &oidn_pass)
{
set_pass(oidn_filter, "output", oidn_pass);
}
/* Scale output pass to match adaptive sampling per-pixel scale, as well as bring alpha channel
* back. */
void postprocess_output(const OIDNPass &oidn_input_pass, const OIDNPass &oidn_output_pass)
{
kernel_assert(oidn_input_pass.num_components == oidn_output_pass.num_components);
const int64_t x = buffer_params_.full_x;
const int64_t y = buffer_params_.full_y;
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
const int64_t offset = buffer_params_.offset;
const int64_t stride = buffer_params_.stride;
const int64_t pass_stride = buffer_params_.pass_stride;
const int64_t row_stride = stride * pass_stride;
const int64_t pixel_offset = offset + x + y * stride;
const int64_t buffer_offset = (pixel_offset * pass_stride);
float *buffer_data = render_buffers_->buffer.data();
const bool has_pass_sample_count = (pass_sample_count_ != PASS_UNUSED);
const bool need_scale = has_pass_sample_count || oidn_input_pass.use_compositing;
for (int y = 0; y < height; ++y) {
float *buffer_row = buffer_data + buffer_offset + y * row_stride;
for (int x = 0; x < width; ++x) {
float *buffer_pixel = buffer_row + x * pass_stride;
float *denoised_pixel = buffer_pixel + oidn_output_pass.offset;
if (need_scale) {
const float pixel_scale = has_pass_sample_count ?
__float_as_uint(buffer_pixel[pass_sample_count_]) :
num_samples_;
denoised_pixel[0] = denoised_pixel[0] * pixel_scale;
denoised_pixel[1] = denoised_pixel[1] * pixel_scale;
denoised_pixel[2] = denoised_pixel[2] * pixel_scale;
}
if (oidn_output_pass.num_components == 3) {
/* Pass without alpha channel. */
}
else if (!oidn_input_pass.use_compositing) {
/* Currently compositing passes are either 3-component (derived by dividing light passes)
* or do not have transparency (shadow catcher). Implicitly rely on this logic, as it
* simplifies logic and avoids extra memory allocation. */
const float *noisy_pixel = buffer_pixel + oidn_input_pass.offset;
denoised_pixel[3] = noisy_pixel[3];
}
else {
/* Assigning to zero since this is a default alpha value for 3-component passes, and it
* is an opaque pixel for 4 component passes. */
denoised_pixel[3] = 0;
}
}
}
}
bool is_pass_scale_needed(OIDNPass &oidn_pass) const
{
if (pass_sample_count_ != PASS_UNUSED) {
/* With adaptive sampling pixels will have different number of samples in them, so need to
* always scale the pass to make pixels uniformly sampled. */
return true;
}
if (!oidn_pass.need_scale) {
return false;
}
if (num_samples_ == 1) {
/* If the avoid scaling if there is only one sample, to save up time (so we don't divide
* buffer by 1). */
return false;
}
return true;
}
void scale_pass_in_render_buffers(OIDNPass &oidn_pass)
{
const int64_t x = buffer_params_.full_x;
const int64_t y = buffer_params_.full_y;
const int64_t width = buffer_params_.width;
const int64_t height = buffer_params_.height;
const int64_t offset = buffer_params_.offset;
const int64_t stride = buffer_params_.stride;
const int64_t pass_stride = buffer_params_.pass_stride;
const int64_t row_stride = stride * pass_stride;
const int64_t pixel_offset = offset + x + y * stride;
const int64_t buffer_offset = (pixel_offset * pass_stride);
float *buffer_data = render_buffers_->buffer.data();
const bool has_pass_sample_count = (pass_sample_count_ != PASS_UNUSED);
for (int y = 0; y < height; ++y) {
float *buffer_row = buffer_data + buffer_offset + y * row_stride;
for (int x = 0; x < width; ++x) {
float *buffer_pixel = buffer_row + x * pass_stride;
float *pass_pixel = buffer_pixel + oidn_pass.offset;
const float pixel_scale = 1.0f / (has_pass_sample_count ?
__float_as_uint(buffer_pixel[pass_sample_count_]) :
num_samples_);
pass_pixel[0] = pass_pixel[0] * pixel_scale;
pass_pixel[1] = pass_pixel[1] * pixel_scale;
pass_pixel[2] = pass_pixel[2] * pixel_scale;
}
}
}
OIDNDenoiser *denoiser_ = nullptr;
const DenoiseParams &denoise_params_;
const BufferParams &buffer_params_;
RenderBuffers *render_buffers_ = nullptr;
int num_samples_ = 0;
bool allow_inplace_modification_ = false;
int pass_sample_count_ = PASS_UNUSED;
/* Optional albedo and normal passes, reused by denoising of different pass types. */
OIDNPass oidn_albedo_pass_;
OIDNPass oidn_normal_pass_;
/* For passes which don't need albedo channel for denoising we replace the actual albedo with
* the (0.5, 0.5, 0.5). This flag indicates that the real albedo pass has been replaced with
* the fake values and denoising of passes which do need albedo can no longer happen. */
bool albedo_replaced_with_fake_ = false;
};
bool OIDNDenoiser::commit_and_execute_filter(OIDNFilter filter)
{
const char *error_message = nullptr;
oidnCommitFilter(filter);
oidnExecuteFilter(filter);
const OIDNError err = oidnGetDeviceError(base_.oidn_device_, &error_message);
if (err == OIDN_ERROR_NONE || err == OIDN_ERROR_CANCELLED) {
return true;
}
if (error_message == nullptr) {
error_message = "Unspecified OIDN error";
}
LOG_ERROR << "OIDN error: " << error_message;
set_error(error_message);
return false;
}
bool OIDNDenoiser::denoise_create_if_needed(const OIDNDenoiseContext &context)
{
/* Create device on first call if it doesn't exist yet. */
if (!base_.oidn_device_) {
base_.oidn_device_ = oidnNewDevice(OIDN_DEVICE_TYPE_CPU);
if (!base_.oidn_device_) {
set_error("Failed to create OIDN CPU device");
return false;
}
oidnSetDeviceBool(base_.oidn_device_, "setAffinity", false);
oidnCommitDevice(base_.oidn_device_);
base_.load_custom_weights();
}
const bool use_pass_albedo = params_.prefilter == DENOISER_PREFILTER_ACCURATE &&
(context.denoise_params_.passes & DENOISER_PASS_ALBEDO) != 0;
const bool use_pass_normal = params_.prefilter == DENOISER_PREFILTER_ACCURATE &&
(context.denoise_params_.passes & DENOISER_PASS_NORMAL) != 0;
const bool recreate_filter = (base_.oidn_filter_ == nullptr) ||
(base_.use_pass_albedo_ != use_pass_albedo) ||
(base_.use_pass_normal_ != use_pass_normal) ||
(base_.quality_ != params_.quality);
if (!recreate_filter) {
return true;
}
if (base_.albedo_filter_) {
oidnReleaseFilter(base_.albedo_filter_);
base_.albedo_filter_ = nullptr;
}
if (base_.normal_filter_) {
oidnReleaseFilter(base_.normal_filter_);
base_.normal_filter_ = nullptr;
}
if (base_.oidn_filter_) {
oidnReleaseFilter(base_.oidn_filter_);
base_.oidn_filter_ = nullptr;
}
if (!base_.create_filters(params_.quality, use_pass_albedo, use_pass_normal)) {
return false;
}
oidnSetFilterProgressMonitorFunction(base_.oidn_filter_, oidn_progress_monitor_function, this);
if (base_.albedo_filter_) {
oidnSetFilterProgressMonitorFunction(
base_.albedo_filter_, oidn_progress_monitor_function, this);
}
if (base_.normal_filter_) {
oidnSetFilterProgressMonitorFunction(
base_.normal_filter_, oidn_progress_monitor_function, this);
}
return true;
}
bool OIDNDenoiser::denoise_run(OIDNDenoiseContext &context, const PassType pass_type)
{
return context.denoise_pass(pass_type);
}
static unique_ptr<DeviceQueue> create_device_queue(const RenderBuffers *render_buffers)
{
Device *device = render_buffers->buffer.device;
if (device->info.has_gpu_queue) {
return device->gpu_queue_create();
}
return nullptr;
}
static void copy_render_buffers_from_device(unique_ptr<DeviceQueue> &queue,
RenderBuffers *render_buffers)
{
if (queue) {
queue->copy_from_device(render_buffers->buffer);
queue->synchronize();
}
else {
render_buffers->copy_from_device();
}
}
static void copy_render_buffers_to_device(unique_ptr<DeviceQueue> &queue,
RenderBuffers *render_buffers)
{
if (queue) {
queue->copy_to_device(render_buffers->buffer);
queue->synchronize();
}
else {
render_buffers->copy_to_device();
}
}
#endif
bool OIDNDenoiser::denoise_buffer(const BufferParams &buffer_params,
const BufferParams & /*denoised_buffer_params*/,
RenderBuffers *render_buffers,
const int num_samples,
const bool allow_inplace_modification,
const float2 /*pixel_jitter*/)
{
DCHECK(openimagedenoise_supported())
<< "OpenImageDenoise is not supported on this platform or build.";
#ifdef WITH_OPENIMAGEDENOISE
const thread_scoped_lock lock(mutex_);
/* Make sure the host-side data is available for denoising. */
unique_ptr<DeviceQueue> queue = create_device_queue(render_buffers);
copy_render_buffers_from_device(queue, render_buffers);
OIDNDenoiseContext context(
this, params_, buffer_params, render_buffers, num_samples, allow_inplace_modification);
if (context.need_denoising()) {
context.read_guiding_passes();
if (!denoise_create_if_needed(context)) {
return false;
}
if (!base_.denoise_configure_if_needed(context.buffer_params_.width,
context.buffer_params_.height))
{
return false;
}
const std::array<PassType, 3> passes = {
{/* Passes which will use real albedo when it is available. */
PASS_COMBINED,
PASS_SHADOW_CATCHER_MATTE,
/* Passes which do not need albedo and hence if real is present it needs to become fake.
*/
PASS_SHADOW_CATCHER}};
for (const PassType pass_type : passes) {
if (!denoise_run(context, pass_type)) {
return false;
}
if (is_cancelled()) {
return false;
}
}
/* TODO: It may be possible to avoid this copy, but we have to ensure that when other code
* copies data from the device it doesn't overwrite the denoiser buffers. */
copy_render_buffers_to_device(queue, render_buffers);
}
#else
(void)buffer_params;
(void)render_buffers;
(void)num_samples;
(void)allow_inplace_modification;
#endif
/* This code is not supposed to run when compiled without OIDN support, so can assume if we made
* it up here all passes are properly denoised. */
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,45 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/denoiser.h"
#include "integrator/denoiser_oidn_base.h"
#include "util/openimagedenoise.h"
#include "util/thread.h"
CCL_NAMESPACE_BEGIN
/* Implementation of a CPU based denoiser which uses OpenImageDenoise library. */
class OIDNDenoiser : public Denoiser {
public:
/* Forwardly declared state which might be using compile-flag specific fields, such as
* OpenImageDenoise device and filter handles. */
class State;
OIDNDenoiser(Device *denoiser_device, const DenoiseParams &params);
bool denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
int num_samples,
bool allow_inplace_modification,
float2 pixel_jitter) override;
#ifdef WITH_OPENIMAGEDENOISE
OIDNDenoiserBase base_;
bool denoise_create_if_needed(const class OIDNDenoiseContext &context);
bool denoise_run(class OIDNDenoiseContext &context, const PassType pass_type);
bool commit_and_execute_filter(OIDNFilter filter);
void set_common_filter_params(OIDNFilter filter);
#endif
protected:
/* We only perform one denoising at a time, since OpenImageDenoise itself is multithreaded.
* Use this mutex whenever images are passed to the OIDN and needs to be denoised. */
static thread_mutex mutex_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,145 @@
/* SPDX-FileCopyrightText: 2011-2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#ifdef WITH_OPENIMAGEDENOISE
# include "integrator/denoiser_oidn_base.h"
# include "util/log.h"
# include "util/path.h"
# if OIDN_VERSION_MAJOR < 2
# define oidnSetFilterInt oidnSetFilter1i
# endif
CCL_NAMESPACE_BEGIN
OIDNDenoiserBase::OIDNDenoiserBase(Denoiser *denoiser) : denoiser_(denoiser) {}
OIDNFilter OIDNDenoiserBase::new_filter()
{
OIDNFilter filter = oidnNewFilter(oidn_device_, "RT");
if (filter == nullptr) {
const char *error_message = nullptr;
const OIDNError err = oidnGetDeviceError(oidn_device_, &error_message);
if (OIDN_ERROR_NONE != err) {
LOG_ERROR << "OIDN error: " << error_message;
denoiser_->set_error(error_message);
}
}
else {
# if OIDN_VERSION_MAJOR >= 2
switch (quality_) {
case DENOISER_QUALITY_FAST:
# if OIDN_VERSION >= 20300
oidnSetFilterInt(filter, "quality", OIDN_QUALITY_FAST);
break;
# endif
case DENOISER_QUALITY_BALANCED:
oidnSetFilterInt(filter, "quality", OIDN_QUALITY_BALANCED);
break;
case DENOISER_QUALITY_HIGH:
default:
oidnSetFilterInt(filter, "quality", OIDN_QUALITY_HIGH);
}
# endif
oidnSetFilterBool(filter, "srgb", false);
/* Set custom weights if available. */
if (!custom_weights_.empty()) {
oidnSetSharedFilterData(filter, "weights", custom_weights_.data(), custom_weights_.size());
}
}
return filter;
}
bool OIDNDenoiserBase::create_filters(DenoiserQuality quality, bool use_albedo, bool use_normal)
{
quality_ = quality;
oidn_filter_ = new_filter();
if (oidn_filter_ == nullptr) {
return false;
}
oidnSetFilterBool(oidn_filter_, "hdr", true);
if (use_albedo) {
albedo_filter_ = new_filter();
if (albedo_filter_ == nullptr) {
return false;
}
}
if (use_normal) {
normal_filter_ = new_filter();
if (normal_filter_ == nullptr) {
return false;
}
}
/* OIDN denoiser handle was created with the requested number of input passes. */
use_pass_albedo_ = use_albedo;
use_pass_normal_ = use_normal;
/* OIDN denoiser has been created, but it needs configuration. */
is_configured_ = false;
return true;
}
void OIDNDenoiserBase::load_custom_weights()
{
const char *custom_weight_path = getenv("CYCLES_OIDN_CUSTOM_WEIGHTS");
if (!custom_weight_path) {
return;
}
if (!path_read_binary(custom_weight_path, custom_weights_)) {
LOG_ERROR << "Failed to load custom OpenImageDenoise weights";
}
}
void OIDNDenoiserBase::release_all_resources()
{
if (albedo_filter_) {
oidnReleaseFilter(albedo_filter_);
albedo_filter_ = nullptr;
}
if (normal_filter_) {
oidnReleaseFilter(normal_filter_);
normal_filter_ = nullptr;
}
if (oidn_filter_) {
oidnReleaseFilter(oidn_filter_);
oidn_filter_ = nullptr;
}
if (oidn_device_) {
oidnReleaseDevice(oidn_device_);
oidn_device_ = nullptr;
}
is_configured_ = false;
use_pass_albedo_ = false;
use_pass_normal_ = false;
}
bool OIDNDenoiserBase::denoise_configure_if_needed(int width, int height)
{
const int2 size = make_int2(width, height);
if (is_configured_ && configured_size_.x == size.x && configured_size_.y == size.y) {
return true;
}
configured_size_ = size;
is_configured_ = true;
return true;
}
CCL_NAMESPACE_END
#endif /* WITH_OPENIMAGEDENOISE */

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2011-2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#ifdef WITH_OPENIMAGEDENOISE
# include "integrator/denoiser.h"
# include "util/openimagedenoise.h"
# include "util/vector.h"
CCL_NAMESPACE_BEGIN
/* Shared OIDN denoiser functionality for both CPU and GPU implementations.
* Uses composition pattern to avoid multiple inheritance. */
class OIDNDenoiserBase {
public:
explicit OIDNDenoiserBase(Denoiser *denoiser);
~OIDNDenoiserBase()
{
release_all_resources();
}
/* OIDN handles and state. */
OIDNDevice oidn_device_ = nullptr;
OIDNFilter oidn_filter_ = nullptr;
OIDNFilter albedo_filter_ = nullptr;
OIDNFilter normal_filter_ = nullptr;
DenoiserQuality quality_ = DENOISER_QUALITY_HIGH;
bool is_configured_ = false;
int2 configured_size_ = make_int2(0, 0);
vector<uint8_t> custom_weights_;
bool use_pass_albedo_ = false;
bool use_pass_normal_ = false;
/* Shared methods. */
bool create_filters(DenoiserQuality quality, bool denoise_abledo, bool denoise_normal);
void load_custom_weights();
void release_all_resources();
bool denoise_configure_if_needed(int width, int height);
private:
OIDNFilter new_filter();
Denoiser *denoiser_; /* Back-pointer for error reporting. */
};
CCL_NAMESPACE_END
#endif /* WITH_OPENIMAGEDENOISE */

View File

@@ -0,0 +1,424 @@
/* SPDX-FileCopyrightText: 2011-2026 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#if defined(WITH_OPENIMAGEDENOISE)
# include "integrator/denoiser_oidn_gpu.h"
# include "device/device.h"
# include "device/oneapi/device_impl.h"
# include "device/queue.h"
# include "session/buffers.h"
# include "util/log.h"
# if OIDN_VERSION_MAJOR < 2
# define oidnSetFilterBool oidnSetFilter1b
# define oidnSetFilterInt oidnSetFilter1i
# define oidnExecuteFilterAsync oidnExecuteFilter
# endif
CCL_NAMESPACE_BEGIN
# if OIDN_VERSION < 20300
static const char *oidn_device_type_to_string(const OIDNDeviceType type)
{
switch (type) {
case OIDN_DEVICE_TYPE_DEFAULT:
return "DEFAULT";
case OIDN_DEVICE_TYPE_CPU:
return "CPU";
/* The initial GPU support was added in OIDN 2.0. */
# if OIDN_VERSION_MAJOR >= 2
case OIDN_DEVICE_TYPE_SYCL:
return "SYCL";
case OIDN_DEVICE_TYPE_CUDA:
return "CUDA";
case OIDN_DEVICE_TYPE_HIP:
return "HIP";
# endif
/* The Metal support was added in OIDN 2.2. */
# if (OIDN_VERSION_MAJOR > 2) || ((OIDN_VERSION_MAJOR == 2) && (OIDN_VERSION_MINOR >= 2))
case OIDN_DEVICE_TYPE_METAL:
return "METAL";
# endif
}
return "UNKNOWN";
}
# endif
bool OIDNDenoiserGPU::is_device_supported(const DeviceInfo &device)
{
# if OIDN_VERSION >= 20300
if (device.type == DEVICE_MULTI) {
for (const DeviceInfo &multi_device : device.multi_devices) {
if (multi_device.type != DEVICE_CPU && multi_device.denoisers & DENOISER_OPENIMAGEDENOISE) {
return true;
}
}
return false;
}
return device.denoisers & DENOISER_OPENIMAGEDENOISE;
# else
if (device.type == DEVICE_MULTI) {
for (const DeviceInfo &multi_device : device.multi_devices) {
if (multi_device.type != DEVICE_CPU && is_device_supported(multi_device)) {
return true;
}
}
return false;
}
LOG_TRACE << "Checking device " << device.description << " (" << device.id
<< ") for OIDN GPU support";
int device_type = OIDN_DEVICE_TYPE_DEFAULT;
switch (device.type) {
# ifdef OIDN_DEVICE_SYCL
case DEVICE_ONEAPI:
device_type = OIDN_DEVICE_TYPE_SYCL;
break;
# endif
# ifdef OIDN_DEVICE_HIP
case DEVICE_HIP:
device_type = OIDN_DEVICE_TYPE_HIP;
break;
# endif
# ifdef OIDN_DEVICE_CUDA
case DEVICE_CUDA:
case DEVICE_OPTIX:
device_type = OIDN_DEVICE_TYPE_CUDA;
break;
# endif
# ifdef OIDN_DEVICE_METAL
case DEVICE_METAL: {
const int num_devices = oidnGetNumPhysicalDevices();
LOG_TRACE << "Found " << num_devices << " OIDN device(s)";
for (int i = 0; i < num_devices; i++) {
const int type = oidnGetPhysicalDeviceInt(i, "type");
const char *name = oidnGetPhysicalDeviceString(i, "name");
LOG_TRACE << "OIDN device " << i << ": name=\"" << name
<< "\", type=" << oidn_device_type_to_string(OIDNDeviceType(type));
if (type == OIDN_DEVICE_TYPE_METAL) {
if (device.id.find(name) != std::string::npos) {
LOG_TRACE << "OIDN device name matches the Cycles device name";
return true;
}
}
}
LOG_TRACE << "No matched OIDN device found";
return false;
}
# endif
case DEVICE_CPU:
/* This is the GPU denoiser - CPU devices shouldn't end up here. */
assert(0);
default:
return false;
}
/* Match GPUs by their PCI ID. */
const int num_devices = oidnGetNumPhysicalDevices();
LOG_TRACE << "Found " << num_devices << " OIDN device(s)";
for (int i = 0; i < num_devices; i++) {
const int type = oidnGetPhysicalDeviceInt(i, "type");
const char *name = oidnGetPhysicalDeviceString(i, "name");
LOG_TRACE << "OIDN device " << i << ": name=\"" << name
<< "\" type=" << oidn_device_type_to_string(OIDNDeviceType(type));
if (type == device_type) {
if (oidnGetPhysicalDeviceBool(i, "pciAddressSupported")) {
unsigned int pci_domain = oidnGetPhysicalDeviceInt(i, "pciDomain");
unsigned int pci_bus = oidnGetPhysicalDeviceInt(i, "pciBus");
unsigned int pci_device = oidnGetPhysicalDeviceInt(i, "pciDevice");
string pci_id = string_printf("%04x:%02x:%02x", pci_domain, pci_bus, pci_device);
LOG_INFO << "OIDN device PCI-e identifier: " << pci_id;
if (device.id.find(pci_id) != string::npos) {
LOG_TRACE << "OIDN device PCI-e identifier matches the Cycles device ID";
return true;
}
}
else {
LOG_TRACE << "Device does not support pciAddressSupported";
}
}
}
LOG_TRACE << "No matched OIDN device found";
return false;
# endif
}
OIDNDenoiserGPU::OIDNDenoiserGPU(Device *denoiser_device, const DenoiseParams &params)
: DenoiserGPU(denoiser_device, params), base_(this)
{
DCHECK_EQ(params.type, DENOISER_OPENIMAGEDENOISE);
}
bool OIDNDenoiserGPU::commit_and_execute_filter(OIDNFilter filter, ExecMode mode)
{
const char *error_message = nullptr;
OIDNError err = OIDN_ERROR_NONE;
for (;;) {
oidnCommitFilter(filter);
if (mode == ExecMode::ASYNC) {
oidnExecuteFilterAsync(filter);
}
else {
oidnExecuteFilter(filter);
}
/* If OIDN runs out of memory, reduce mem limit and retry */
err = oidnGetDeviceError(base_.oidn_device_, &error_message);
if (err != OIDN_ERROR_OUT_OF_MEMORY || max_mem_ < 200) {
break;
}
max_mem_ = max_mem_ / 2;
oidnSetFilterInt(filter, "maxMemoryMB", max_mem_);
}
if (err != OIDN_ERROR_NONE) {
if (error_message == nullptr) {
error_message = "Unspecified OIDN error";
}
LOG_ERROR << "OIDN error: " << error_message;
set_error(error_message);
return false;
}
return true;
}
bool OIDNDenoiserGPU::denoise_create_if_needed(DenoiseContext &context)
{
const bool use_pass_albedo = (context.denoise_params.passes & DENOISER_PASS_ALBEDO) != 0;
const bool use_pass_normal = (context.denoise_params.passes & DENOISER_PASS_NORMAL) != 0;
const bool recreate_denoiser = (base_.oidn_device_ == nullptr) ||
(base_.oidn_filter_ == nullptr) ||
(base_.use_pass_albedo_ != use_pass_albedo) ||
(base_.use_pass_normal_ != use_pass_normal) ||
(base_.quality_ != params_.quality);
if (!recreate_denoiser) {
return true;
}
/* Destroy existing handles before creating new ones. */
base_.release_all_resources();
switch (denoiser_device_->info.type) {
# if defined(OIDN_DEVICE_SYCL) && defined(WITH_ONEAPI)
case DEVICE_ONEAPI:
base_.oidn_device_ = oidnNewSYCLDevice(
(const sycl::queue *)reinterpret_cast<OneapiDevice *>(denoiser_device_)->sycl_queue(),
1);
break;
# endif
# if defined(OIDN_DEVICE_METAL) && defined(WITH_METAL)
case DEVICE_METAL: {
denoiser_queue_->init_execution();
const MTLCommandQueue_id queue = (const MTLCommandQueue_id)denoiser_queue_->native_queue();
base_.oidn_device_ = oidnNewMetalDevice(&queue, 1);
} break;
# endif
# if defined(OIDN_DEVICE_CUDA) && defined(WITH_CUDA)
case DEVICE_CUDA:
case DEVICE_OPTIX: {
/* Directly using the stream from the DeviceQueue returns "invalid resource handle". */
cudaStream_t stream = nullptr;
base_.oidn_device_ = oidnNewCUDADevice(&denoiser_device_->info.num, &stream, 1);
break;
}
# endif
# if defined(OIDN_DEVICE_HIP) && defined(WITH_HIP)
case DEVICE_HIP: {
hipStream_t stream = nullptr;
base_.oidn_device_ = oidnNewHIPDevice(&denoiser_device_->info.num, &stream, 1);
break;
}
# endif
default:
break;
}
if (!base_.oidn_device_) {
set_error("Failed to create OIDN device");
return false;
}
if (denoiser_queue_) {
denoiser_queue_->init_execution();
}
oidnCommitDevice(base_.oidn_device_);
base_.load_custom_weights();
return base_.create_filters(params_.quality, use_pass_albedo, use_pass_normal);
}
bool OIDNDenoiserGPU::denoise_configure_if_needed(DenoiseContext &context)
{
/* Limit maximum tile size denoiser can be invoked with. */
return base_.denoise_configure_if_needed(context.buffer_params.width,
context.buffer_params.height);
}
bool OIDNDenoiserGPU::denoise_run(const DenoiseContext &context, const DenoisePass &pass)
{
/* Color pass. */
const int64_t pass_stride_in_bytes = context.buffer_params.pass_stride * sizeof(float);
set_filter_pass(base_.oidn_filter_,
"color",
context.render_buffers->buffer.device_pointer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
pass.denoised_offset * sizeof(float),
pass_stride_in_bytes,
pass_stride_in_bytes * context.buffer_params.stride);
set_filter_pass(base_.oidn_filter_,
"output",
context.render_buffers->buffer.device_pointer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
pass.denoised_offset * sizeof(float),
pass_stride_in_bytes,
pass_stride_in_bytes * context.buffer_params.stride);
/* Optional albedo and color passes. */
const device_ptr d_guiding_buffer = context.guiding_params.device_pointer;
const int64_t pixel_stride_in_bytes = context.guiding_params.pass_stride * sizeof(float);
const int64_t row_stride_in_bytes = context.guiding_params.stride * pixel_stride_in_bytes;
if (base_.use_pass_albedo_) {
set_filter_pass(base_.oidn_filter_,
"albedo",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_albedo * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
if (params_.prefilter == DENOISER_PREFILTER_ACCURATE) {
set_filter_pass(base_.albedo_filter_,
"albedo",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_albedo * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
set_filter_pass(base_.albedo_filter_,
"output",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_albedo * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
if (!commit_and_execute_filter(base_.albedo_filter_, ExecMode::ASYNC)) {
return false;
}
}
}
if (base_.use_pass_normal_) {
set_filter_pass(base_.oidn_filter_,
"normal",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_normal * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
if (params_.prefilter == DENOISER_PREFILTER_ACCURATE) {
set_filter_pass(base_.normal_filter_,
"normal",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_normal * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
set_filter_pass(base_.normal_filter_,
"output",
d_guiding_buffer,
OIDN_FORMAT_FLOAT3,
context.buffer_params.width,
context.buffer_params.height,
context.guiding_params.pass_normal * sizeof(float),
pixel_stride_in_bytes,
row_stride_in_bytes);
if (!commit_and_execute_filter(base_.normal_filter_, ExecMode::ASYNC)) {
return false;
}
}
}
oidnSetFilterInt(base_.oidn_filter_, "cleanAux", params_.prefilter != DENOISER_PREFILTER_FAST);
return commit_and_execute_filter(base_.oidn_filter_);
}
void OIDNDenoiserGPU::set_filter_pass(OIDNFilter filter,
const char *name,
device_ptr ptr,
const int format,
const int width,
const int height,
const size_t offset_in_bytes,
const size_t pixel_stride_in_bytes,
const size_t row_stride_in_bytes)
{
# if defined(OIDN_DEVICE_METAL) && defined(WITH_METAL)
if (denoiser_device_->info.type == DEVICE_METAL) {
void *mtl_buffer = denoiser_device_->get_native_buffer(ptr);
OIDNBuffer oidn_buffer = oidnNewSharedBufferFromMetal(base_.oidn_device_, mtl_buffer);
oidnSetFilterImage(filter,
name,
oidn_buffer,
(OIDNFormat)format,
width,
height,
offset_in_bytes,
pixel_stride_in_bytes,
row_stride_in_bytes);
oidnReleaseBuffer(oidn_buffer);
}
else
# endif
{
oidnSetSharedFilterImage(filter,
name,
(void *)ptr,
(OIDNFormat)format,
width,
height,
offset_in_bytes,
pixel_stride_in_bytes,
row_stride_in_bytes);
}
}
CCL_NAMESPACE_END
#endif

View File

@@ -0,0 +1,61 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#if defined(WITH_OPENIMAGEDENOISE)
# include "integrator/denoiser_gpu.h"
# include "integrator/denoiser_oidn_base.h"
# include "util/openimagedenoise.h" // IWYU pragma: keep
CCL_NAMESPACE_BEGIN
/* Implementation of a GPU denoiser which uses OpenImageDenoise library. */
class OIDNDenoiserGPU : public DenoiserGPU {
public:
class State;
OIDNDenoiserGPU(Device *denoiser_device, const DenoiseParams &params);
static bool is_device_supported(const DeviceInfo &device);
protected:
enum class ExecMode {
SYNC,
ASYNC,
};
/* Create OIDN denoiser descriptor if needed.
* Will do nothing if the current OIDN descriptor is usable for the given parameters.
* If the OIDN denoiser descriptor did re-allocate here it is left unconfigured. */
bool denoise_create_if_needed(DenoiseContext &context) override;
/* Configure existing OIDN denoiser descriptor for the use for the given task. */
bool denoise_configure_if_needed(DenoiseContext &context) override;
/* Run configured denoiser. */
bool denoise_run(const DenoiseContext &context, const DenoisePass &pass) override;
bool commit_and_execute_filter(OIDNFilter filter, ExecMode mode = ExecMode::SYNC);
void set_filter_pass(OIDNFilter filter,
const char *name,
device_ptr ptr,
const int format,
const int width,
const int height,
const size_t offset_in_bytes,
const size_t pixel_stride_in_bytes,
size_t row_stride_in_bytes);
OIDNDenoiserBase base_;
/* Filter memory usage limit if we ran out of memory with OIDN's default limit. */
int max_mem_ = 768;
};
CCL_NAMESPACE_END
#endif

View File

@@ -0,0 +1,281 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#ifdef WITH_OPTIX
# include "integrator/denoiser_optix.h"
# include "integrator/pass_accessor_gpu.h"
# include "device/optix/device_impl.h"
# include "device/optix/queue.h"
# include <optix_denoiser_tiling.h>
CCL_NAMESPACE_BEGIN
OptiXDenoiser::OptiXDenoiser(Device *denoiser_device, const DenoiseParams &params)
: DenoiserGPU(denoiser_device, params), state_(denoiser_device, "__denoiser_state", true)
{
}
OptiXDenoiser::~OptiXDenoiser()
{
/* It is important that the OptixDenoiser handle is destroyed before the OptixDeviceContext
* handle, which is guaranteed since the local denoising device owning the OptiX device context
* is deleted as part of the Denoiser class destructor call after this. */
if (optix_denoiser_ != nullptr) {
optixDenoiserDestroy(optix_denoiser_);
}
}
bool OptiXDenoiser::is_device_supported(const DeviceInfo &device)
{
if (device.type == DEVICE_OPTIX) {
return device.denoisers & DENOISER_OPTIX;
}
return false;
}
bool OptiXDenoiser::denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
const int num_samples,
const bool allow_inplace_modification,
const float2 pixel_jitter)
{
OptiXDevice *const optix_device = static_cast<OptiXDevice *>(denoiser_device_);
const CUDAContextScope scope(optix_device);
return DenoiserGPU::denoise_buffer(buffer_params,
denoised_buffer_params,
render_buffers,
num_samples,
allow_inplace_modification,
pixel_jitter);
}
bool OptiXDenoiser::denoise_create_if_needed(DenoiseContext &context)
{
const bool use_pass_albedo = (context.denoise_params.passes & DENOISER_PASS_ALBEDO) != 0;
const bool use_pass_normal = (context.denoise_params.passes & DENOISER_PASS_NORMAL) != 0;
const bool use_pass_motion = context.denoise_params.temporally_stable &&
(context.denoise_params.passes & DENOISER_PASS_MOTION) != 0;
const bool use_upscale_model = context.denoise_params.upscale_factor == 2.0f;
const bool recreate_denoiser = (optix_denoiser_ == nullptr) ||
(use_pass_albedo_ != use_pass_albedo) ||
(use_pass_normal_ != use_pass_normal) ||
(use_pass_motion_ != use_pass_motion) ||
(use_upscale_model_ != use_upscale_model);
if (!recreate_denoiser) {
return true;
}
/* Destroy existing handle before creating new one. */
if (optix_denoiser_) {
optixDenoiserDestroy(optix_denoiser_);
}
/* Create OptiX denoiser handle on demand when it is first used. */
OptixDenoiserOptions denoiser_options = {};
denoiser_options.guideAlbedo = use_pass_albedo;
denoiser_options.guideNormal = use_pass_normal;
OptixDenoiserModelKind model = OPTIX_DENOISER_MODEL_KIND_AOV;
if (use_pass_motion) {
if (use_upscale_model) {
model = OPTIX_DENOISER_MODEL_KIND_TEMPORAL_UPSCALE2X;
}
else {
model = OPTIX_DENOISER_MODEL_KIND_TEMPORAL;
}
}
else {
if (use_upscale_model) {
model = OPTIX_DENOISER_MODEL_KIND_UPSCALE2X;
}
}
const OptixResult result = optixDenoiserCreate(
static_cast<OptiXDevice *>(denoiser_device_)->context,
model,
&denoiser_options,
&optix_denoiser_);
if (result != OPTIX_SUCCESS) {
set_error("Failed to create OptiX denoiser");
return false;
}
/* OptiX denoiser handle was created with the requested number of input passes. */
use_pass_albedo_ = use_pass_albedo;
use_pass_normal_ = use_pass_normal;
use_pass_motion_ = use_pass_motion;
use_upscale_model_ = use_upscale_model;
/* OptiX denoiser has been created, but it needs configuration. */
is_configured_ = false;
return true;
}
bool OptiXDenoiser::denoise_configure_if_needed(DenoiseContext &context)
{
/* Limit maximum tile size denoiser can be invoked with. */
const int2 tile_size = make_int2(min(context.buffer_params.width, 4096),
min(context.buffer_params.height, 4096));
if (is_configured_ && (configured_size_.x == tile_size.x && configured_size_.y == tile_size.y)) {
return true;
}
optix_device_assert(
denoiser_device_,
optixDenoiserComputeMemoryResources(optix_denoiser_, tile_size.x, tile_size.y, &sizes_));
const bool tiled = tile_size.x < context.buffer_params.width ||
tile_size.y < context.buffer_params.height;
/* Allocate denoiser state if tile size has changed since last setup. */
state_.device = denoiser_device_;
state_.alloc_to_device(sizes_.stateSizeInBytes + sizes_.withOverlapScratchSizeInBytes);
/* Initialize denoiser state for the current tile size. */
const OptixResult result = optixDenoiserSetup(
optix_denoiser_,
0, /* Work around bug in r495 drivers that causes artifacts when denoiser setup is called
* on a stream that is not the default stream. */
tile_size.x + (tiled ? sizes_.overlapWindowSizeInPixels * 2 : 0),
tile_size.y + (tiled ? sizes_.overlapWindowSizeInPixels * 2 : 0),
state_.device_pointer,
sizes_.stateSizeInBytes,
state_.device_pointer + sizes_.stateSizeInBytes,
sizes_.withOverlapScratchSizeInBytes);
if (result != OPTIX_SUCCESS) {
set_error("Failed to set up OptiX denoiser");
return false;
}
cuda_device_assert(denoiser_device_, cuCtxSynchronize());
is_configured_ = true;
configured_size_ = tile_size;
return true;
}
bool OptiXDenoiser::denoise_run(const DenoiseContext &context, const DenoisePass &pass)
{
/* Set up input and output layer information. */
OptixImage2D color_layer = {0};
OptixImage2D albedo_layer = {0};
OptixImage2D normal_layer = {0};
OptixImage2D flow_layer = {0};
OptixImage2D output_layer = {0};
OptixImage2D prev_output_layer = {0};
/* Color pass. */
{
const int pass_denoised = pass.denoised_offset;
const int64_t pass_stride_in_bytes = context.buffer_params.pass_stride * sizeof(float);
color_layer.data = context.render_buffers->buffer.device_pointer +
pass_denoised * sizeof(float);
color_layer.width = context.buffer_params.width;
color_layer.height = context.buffer_params.height;
color_layer.rowStrideInBytes = pass_stride_in_bytes * context.buffer_params.stride;
color_layer.pixelStrideInBytes = pass_stride_in_bytes;
color_layer.format = OPTIX_PIXEL_FORMAT_FLOAT3;
}
/* Previous output. */
if (use_pass_motion_ && context.prev_output.offset != PASS_UNUSED) {
const int64_t pass_stride_in_bytes = context.prev_output.pass_stride * sizeof(float);
prev_output_layer.data = context.prev_output.device_pointer +
context.prev_output.offset * sizeof(float);
prev_output_layer.width = context.denoised_buffer_params.width;
prev_output_layer.height = context.denoised_buffer_params.height;
prev_output_layer.rowStrideInBytes = pass_stride_in_bytes * context.prev_output.stride;
prev_output_layer.pixelStrideInBytes = pass_stride_in_bytes;
prev_output_layer.format = OPTIX_PIXEL_FORMAT_FLOAT3;
}
/* Optional albedo and color passes. */
const device_ptr d_guiding_buffer = context.guiding_params.device_pointer;
const int64_t pixel_stride_in_bytes = context.guiding_params.pass_stride * sizeof(float);
const int64_t row_stride_in_bytes = context.guiding_params.stride * pixel_stride_in_bytes;
if (use_pass_albedo_) {
albedo_layer.data = d_guiding_buffer + context.guiding_params.pass_albedo * sizeof(float);
albedo_layer.width = context.buffer_params.width;
albedo_layer.height = context.buffer_params.height;
albedo_layer.rowStrideInBytes = row_stride_in_bytes;
albedo_layer.pixelStrideInBytes = pixel_stride_in_bytes;
albedo_layer.format = OPTIX_PIXEL_FORMAT_FLOAT3;
}
if (use_pass_normal_) {
normal_layer.data = d_guiding_buffer + context.guiding_params.pass_normal * sizeof(float);
normal_layer.width = context.buffer_params.width;
normal_layer.height = context.buffer_params.height;
normal_layer.rowStrideInBytes = row_stride_in_bytes;
normal_layer.pixelStrideInBytes = pixel_stride_in_bytes;
normal_layer.format = OPTIX_PIXEL_FORMAT_FLOAT3;
}
if (use_pass_motion_) {
flow_layer.data = d_guiding_buffer + context.guiding_params.pass_flow * sizeof(float);
flow_layer.width = context.buffer_params.width;
flow_layer.height = context.buffer_params.height;
flow_layer.rowStrideInBytes = row_stride_in_bytes;
flow_layer.pixelStrideInBytes = pixel_stride_in_bytes;
flow_layer.format = OPTIX_PIXEL_FORMAT_FLOAT2;
}
/* Denoise in-place of the noisy input in the render buffers. */
{
output_layer = color_layer;
output_layer.width = context.denoised_buffer_params.width;
output_layer.height = context.denoised_buffer_params.height;
output_layer.rowStrideInBytes = output_layer.pixelStrideInBytes *
context.denoised_buffer_params.stride;
}
OptixDenoiserGuideLayer guide_layers = {};
guide_layers.albedo = albedo_layer;
guide_layers.normal = normal_layer;
guide_layers.flow = flow_layer;
OptixDenoiserLayer image_layers = {};
image_layers.input = color_layer;
image_layers.previousOutput = prev_output_layer;
image_layers.output = output_layer;
/* Finally run denoising. */
OptixDenoiserParams params = {}; /* All parameters are disabled/zero. */
optix_device_assert(denoiser_device_,
optixUtilDenoiserInvokeTiled(
optix_denoiser_,
static_cast<OptiXDeviceQueue *>(denoiser_queue_.get())->stream(),
&params,
state_.device_pointer,
sizes_.stateSizeInBytes,
&guide_layers,
&image_layers,
1,
state_.device_pointer + sizes_.stateSizeInBytes,
sizes_.withOverlapScratchSizeInBytes,
sizes_.overlapWindowSizeInPixels,
configured_size_.x,
configured_size_.y));
return true;
}
CCL_NAMESPACE_END
#endif

View File

@@ -0,0 +1,67 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#ifdef WITH_OPTIX
# include "integrator/denoiser_gpu.h"
# include "device/optix/util.h"
CCL_NAMESPACE_BEGIN
/* Implementation of denoising API which uses the OptiX denoiser. */
class OptiXDenoiser : public DenoiserGPU {
public:
OptiXDenoiser(Device *denoiser_device, const DenoiseParams &params);
~OptiXDenoiser();
virtual bool denoise_buffer(const BufferParams &buffer_params,
const BufferParams &denoised_buffer_params,
RenderBuffers *render_buffers,
int num_samples,
bool allow_inplace_modification,
float2 pixel_jitter) override;
static bool is_device_supported(const DeviceInfo &device);
private:
/* Set fake albedo pixels in the albedo guiding pass storage.
* After this point only passes which do not need albedo for denoising can be processed. */
bool denoise_filter_guiding_set_fake_albedo(const DenoiseContext &context);
/* Create OptiX denoiser descriptor if needed.
* Will do nothing if the current OptiX descriptor is usable for the given parameters.
* If the OptiX denoiser descriptor did re-allocate here it is left unconfigured. */
virtual bool denoise_create_if_needed(DenoiseContext &context) override;
/* Configure existing OptiX denoiser descriptor for the use for the given task. */
virtual bool denoise_configure_if_needed(DenoiseContext &context) override;
/* Run configured denoiser. */
virtual bool denoise_run(const DenoiseContext &context, const DenoisePass &pass) override;
OptixDenoiser optix_denoiser_ = nullptr;
/* Configuration size, as provided to `optixDenoiserSetup`.
* If the `optixDenoiserSetup()` was never used on the current `optix_denoiser` the
* `is_configured` will be false. */
bool is_configured_ = false;
int2 configured_size_ = make_int2(0, 0);
/* OptiX denoiser state and scratch buffers, stored in a single memory buffer.
* The memory layout goes as following: [denoiser state][scratch buffer]. */
device_only_memory<unsigned char> state_;
OptixDenoiserSizes sizes_ = {};
bool use_pass_albedo_ = false;
bool use_pass_normal_ = false;
bool use_pass_motion_ = false;
bool use_upscale_model_ = false;
};
CCL_NAMESPACE_END
#endif

View File

@@ -0,0 +1,37 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
struct GuidingParams {
/* The subset of path guiding parameters that can trigger a creation/rebuild
* of the guiding field. */
bool use = false;
bool use_surface_guiding = false;
bool use_volume_guiding = false;
GuidingDistributionType type = GUIDING_TYPE_PARALLAX_AWARE_VMM;
GuidingDirectionalSamplingType sampling_type = GUIDING_DIRECTIONAL_SAMPLING_TYPE_PRODUCT_MIS;
float roughness_threshold = 0.05f;
int training_samples = 128;
bool deterministic = false;
GuidingParams() = default;
bool modified(const GuidingParams &other) const
{
return !((use == other.use) && (use_surface_guiding == other.use_surface_guiding) &&
(use_volume_guiding == other.use_volume_guiding) && (type == other.type) &&
(sampling_type == other.sampling_type) &&
(training_samples == other.training_samples) &&
(roughness_threshold == other.roughness_threshold) &&
(deterministic == other.deterministic));
}
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,330 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/pass_accessor.h"
#include "kernel/types.h"
#include "session/buffers.h"
#include "util/log.h"
CCL_NAMESPACE_BEGIN
/* --------------------------------------------------------------------
* Pass input information.
*/
PassAccessor::PassAccessInfo::PassAccessInfo(const BufferPass &pass)
: type(pass.type),
mode(pass.mode),
include_albedo(pass.include_albedo),
is_lightgroup(!pass.lightgroup.empty()),
offset(pass.offset)
{
}
/* --------------------------------------------------------------------
* Pass destination.
*/
PassAccessor::Destination::Destination(float *pixels, const int num_components)
: pixels(pixels), num_components(num_components)
{
}
PassAccessor::Destination::Destination(const PassType pass_type, const PassMode pass_mode)
{
const PassInfo pass_info = Pass::get_info(pass_type, pass_mode);
num_components = pass_info.num_components;
}
/* --------------------------------------------------------------------
* Pass source.
*/
PassAccessor::Source::Source(const float *pixels, const int num_components)
: pixels(pixels), num_components(num_components)
{
}
/* --------------------------------------------------------------------
* Pass accessor.
*/
PassAccessor::PassAccessor(const PassAccessInfo &pass_access_info,
const float exposure,
const int num_samples)
: pass_access_info_(pass_access_info), exposure_(exposure), num_samples_(num_samples)
{
}
bool PassAccessor::get_render_tile_pixels(const RenderBuffers *render_buffers,
const Destination &destination) const
{
if (render_buffers == nullptr || render_buffers->buffer.data() == nullptr) {
return false;
}
return get_render_tile_pixels(render_buffers, render_buffers->params, destination);
}
static void pad_pixels(const BufferParams &buffer_params,
const PassAccessor::Destination &destination,
const int src_num_components)
{
/* When requesting a single channel pass as RGBA, or RGB pass as RGBA,
* fill in the additional components for convenience. */
const int dest_num_components = destination.num_components;
if (src_num_components >= dest_num_components) {
return;
}
const size_t size = static_cast<size_t>(buffer_params.width) * buffer_params.height;
if (destination.pixels) {
const size_t pixel_stride = destination.pixel_stride ? destination.pixel_stride :
destination.num_components;
float *pixel = destination.pixels + pixel_stride * destination.offset;
for (size_t i = 0; i < size; i++, pixel += dest_num_components) {
if (dest_num_components >= 3 && src_num_components == 1) {
pixel[1] = pixel[0];
pixel[2] = pixel[0];
}
if (dest_num_components >= 4) {
pixel[3] = 1.0f;
}
}
}
if (destination.pixels_half_rgba) {
const half one = float_to_half_display(1.0f);
half4 *pixel = destination.pixels_half_rgba + destination.offset;
for (size_t i = 0; i < size; i++, pixel++) {
if (dest_num_components >= 3 && src_num_components == 1) {
pixel[0].y = pixel[0].x;
pixel[0].z = pixel[0].x;
}
if (dest_num_components >= 4) {
pixel[0].w = one;
}
}
}
}
bool PassAccessor::get_render_tile_pixels(const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination) const
{
if (render_buffers == nullptr || render_buffers->buffer.data() == nullptr) {
return false;
}
const PassType type = pass_access_info_.type;
const PassMode mode = pass_access_info_.mode;
const PassInfo pass_info = Pass::get_info(
type, mode, pass_access_info_.include_albedo, pass_access_info_.is_lightgroup);
int num_written_components = pass_info.num_components;
if (pass_info.num_components == 1) {
if (is_volume_guiding_pass(type)) {
get_pass_rgbe(render_buffers, buffer_params, destination);
num_written_components = 3;
}
/* Single channel passes. */
else if (mode == PassMode::DENOISED) {
/* Denoised passes store their final pixels, no need in special calculation. */
get_pass_float(render_buffers, buffer_params, destination);
}
else if (type == PASS_DEPTH) {
get_pass_depth(render_buffers, buffer_params, destination);
}
else if (type == PASS_MIST) {
get_pass_mist(render_buffers, buffer_params, destination);
}
else if (type == PASS_VOLUME_MAJORANT) {
get_pass_volume_majorant(render_buffers, buffer_params, destination);
}
else if (type == PASS_SAMPLE_COUNT) {
get_pass_sample_count(render_buffers, buffer_params, destination);
}
else {
get_pass_float(render_buffers, buffer_params, destination);
}
}
else if (type == PASS_MOTION) {
/* Motion pass. */
DCHECK_EQ(destination.num_components, 4) << "Motion pass must have 4 components";
get_pass_motion(render_buffers, buffer_params, destination);
}
else if (type == PASS_CRYPTOMATTE) {
/* Cryptomatte pass. */
DCHECK_EQ(destination.num_components, 4) << "Cryptomatte pass must have 4 components";
get_pass_cryptomatte(render_buffers, buffer_params, destination);
}
else {
/* RGB, RGBA and vector passes. */
DCHECK(destination.num_components == 3 || destination.num_components == 4)
<< pass_type_as_string(type) << " pass must have 3 or 4 components";
if (type == PASS_SHADOW_CATCHER_MATTE && pass_access_info_.use_approximate_shadow_catcher) {
/* Denoised matte with shadow needs to do calculation (will use denoised shadow catcher pass
* to approximate shadow with). */
get_pass_shadow_catcher_matte_with_shadow(render_buffers, buffer_params, destination);
}
else if (type == PASS_SHADOW_CATCHER && mode != PassMode::DENOISED) {
/* Shadow catcher pass. */
get_pass_shadow_catcher(render_buffers, buffer_params, destination);
}
else if ((pass_info.divide_type != PASS_NONE || pass_info.direct_type != PASS_NONE ||
pass_info.indirect_type != PASS_NONE) &&
mode != PassMode::DENOISED)
{
/* RGB lighting passes that need to divide out color and/or sum direct and indirect.
* These can also optionally write alpha like the combined pass. */
get_pass_light_path(render_buffers, buffer_params, destination);
num_written_components = 4;
}
else {
/* Passes that need no special computation, or denoised passes that already
* had the computation done. */
if (pass_info.num_components == 3) {
get_pass_float3(render_buffers, buffer_params, destination);
/* Use alpha for colors passes. */
if (type == PASS_DIFFUSE_COLOR || type == PASS_GLOSSY_COLOR ||
type == PASS_TRANSMISSION_COLOR)
{
num_written_components = destination.num_components;
}
}
else if (pass_info.num_components == 4) {
if (destination.num_components == 3) {
/* Special case for denoiser access of RGBA passes ignoring alpha channel. */
get_pass_float3(render_buffers, buffer_params, destination);
}
else if (type == PASS_COMBINED || type == PASS_SHADOW_CATCHER ||
type == PASS_SHADOW_CATCHER_MATTE)
{
/* Passes with transparency as 4th component. */
get_pass_combined(render_buffers, buffer_params, destination);
}
else {
/* Passes with alpha as 4th component. */
get_pass_float4(render_buffers, buffer_params, destination);
}
}
}
}
pad_pixels(buffer_params, destination, num_written_components);
return true;
}
void PassAccessor::init_kernel_film_convert(KernelFilmConvert *kfilm_convert,
const BufferParams &buffer_params,
const Destination &destination) const
{
const PassType type = pass_access_info_.type;
const PassMode mode = pass_access_info_.mode;
const PassInfo pass_info = Pass::get_info(
type, mode, pass_access_info_.include_albedo, pass_access_info_.is_lightgroup);
kfilm_convert->pass_offset = pass_access_info_.offset;
kfilm_convert->pass_stride = buffer_params.pass_stride;
kfilm_convert->pass_use_exposure = pass_info.use_exposure;
kfilm_convert->pass_use_filter = pass_info.use_filter;
/* TODO(sergey): Some of the passes needs to become denoised when denoised pass is accessed. */
if (pass_info.direct_type != PASS_NONE) {
kfilm_convert->pass_offset = buffer_params.get_pass_offset(pass_info.direct_type);
}
kfilm_convert->pass_indirect = buffer_params.get_pass_offset(pass_info.indirect_type);
kfilm_convert->pass_divide = buffer_params.get_pass_offset(pass_info.divide_type);
kfilm_convert->pass_combined = buffer_params.get_pass_offset(PASS_COMBINED);
kfilm_convert->pass_sample_count = buffer_params.get_pass_offset(PASS_SAMPLE_COUNT);
kfilm_convert->pass_adaptive_aux_buffer = buffer_params.get_pass_offset(
PASS_ADAPTIVE_AUX_BUFFER);
kfilm_convert->pass_motion_weight = buffer_params.get_pass_offset(PASS_MOTION_WEIGHT);
kfilm_convert->pass_shadow_catcher = buffer_params.get_pass_offset(PASS_SHADOW_CATCHER, mode);
kfilm_convert->pass_shadow_catcher_sample_count = buffer_params.get_pass_offset(
PASS_SHADOW_CATCHER_SAMPLE_COUNT);
kfilm_convert->pass_shadow_catcher_matte = buffer_params.get_pass_offset(
PASS_SHADOW_CATCHER_MATTE, mode);
/* Background is not denoised, so always use noisy pass. */
kfilm_convert->pass_background = buffer_params.get_pass_offset(PASS_BACKGROUND);
/* If we have a sample count pass, we must perform the division in the kernel instead
* (unless the sample count pass is the one being read). */
const bool divide_by_samples = (type == PASS_SAMPLE_COUNT) ||
(kfilm_convert->pass_sample_count == PASS_UNUSED);
if (pass_info.use_filter && divide_by_samples) {
kfilm_convert->scale = num_samples_ != 0 ? pass_info.scale / num_samples_ : 0.0f;
}
else {
kfilm_convert->scale = pass_info.scale;
if (!pass_access_info_.use_sample_count) {
kfilm_convert->pass_use_filter = false;
}
}
if (pass_info.use_exposure) {
kfilm_convert->exposure = exposure_;
}
else {
kfilm_convert->exposure = 1.0f;
}
kfilm_convert->scale_exposure = kfilm_convert->scale * kfilm_convert->exposure;
kfilm_convert->use_approximate_shadow_catcher = pass_access_info_.use_approximate_shadow_catcher;
kfilm_convert->use_approximate_shadow_catcher_background =
pass_access_info_.use_approximate_shadow_catcher_background;
kfilm_convert->show_active_pixels = pass_access_info_.show_active_pixels;
kfilm_convert->num_components = destination.num_components;
kfilm_convert->pixel_stride = destination.pixel_stride ? destination.pixel_stride :
destination.num_components;
kfilm_convert->is_denoised = (mode == PassMode::DENOISED);
}
bool PassAccessor::set_render_tile_pixels(RenderBuffers *render_buffers, const Source &source)
{
if (render_buffers == nullptr || render_buffers->buffer.data() == nullptr) {
return false;
}
const PassInfo pass_info = Pass::get_info(pass_access_info_.type,
pass_access_info_.mode,
pass_access_info_.include_albedo,
pass_access_info_.is_lightgroup);
const BufferParams &buffer_params = render_buffers->params;
float *buffer_data = render_buffers->buffer.data();
const int size = buffer_params.width * buffer_params.height;
const int out_stride = buffer_params.pass_stride;
const int in_stride = source.num_components;
const int num_components_to_copy = min(source.num_components, pass_info.num_components);
float *out = buffer_data + pass_access_info_.offset;
const float *in = source.pixels + source.offset * in_stride;
for (int i = 0; i < size; i++, out += out_stride, in += in_stride) {
memcpy(out, in, sizeof(float) * num_components_to_copy);
}
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "scene/pass.h"
#include "util/half.h"
CCL_NAMESPACE_BEGIN
class RenderBuffers;
class BufferPass;
class BufferParams;
struct KernelFilmConvert;
/* Helper class which allows to access pass data.
* Is designed in a way that it is created once when the pass data is known, and then pixels gets
* progressively update from various render buffers. */
class PassAccessor {
public:
class PassAccessInfo {
public:
PassAccessInfo() = default;
explicit PassAccessInfo(const BufferPass &pass);
PassType type = PASS_NONE;
PassMode mode = PassMode::NOISY;
bool include_albedo = false;
bool is_lightgroup = false;
int offset = -1;
bool use_sample_count = true;
/* For the shadow catcher matte pass: whether to approximate shadow catcher pass into its
* matte pass, so that both artificial objects and shadows can be alpha-overed onto a backdrop.
*/
bool use_approximate_shadow_catcher = false;
/* When approximate shadow catcher matte is used alpha-over the result on top of background. */
bool use_approximate_shadow_catcher_background = false;
bool show_active_pixels = false;
};
class Destination {
public:
Destination() = default;
Destination(float *pixels, const int num_components);
/* Destination will be initialized with the number of components which is native for the given
* pass type. */
explicit Destination(const PassType pass_type, const PassMode pass_mode);
/* CPU-side pointers. only usable by the `PassAccessorCPU`. */
float *pixels = nullptr;
half4 *pixels_half_rgba = nullptr;
/* Device-side pointers. */
device_ptr d_pixels = 0;
device_ptr d_pixels_half_rgba = 0;
/* Number of components per pixel in the floating-point destination.
* Is ignored for half4 destination (where number of components is implied to be 4). */
int num_components = 0;
/* Offset in pixels from the beginning of pixels storage.
* Allows to get pixels of render buffer into a partial slice of the destination. */
int offset = 0;
/* Offset in floats from the beginning of pixels storage.
* Is ignored for half4 destination. */
int pixel_offset = 0;
/* Number of floats per pixel. When zero is the same as `num_components`.
*
* NOTE: Is ignored for half4 destination, as the half4 pixels are always 4-component
* half-floats. */
int pixel_stride = 0;
/* Row stride in pixel elements:
* - For the float destination stride is a number of floats per row.
* - For the half4 destination stride is a number of half4 per row. */
int stride = 0;
};
class Source {
public:
Source() = default;
Source(const float *pixels, const int num_components);
/* CPU-side pointers. only usable by the `PassAccessorCPU`. */
const float *pixels = nullptr;
int num_components = 0;
/* Offset in pixels from the beginning of pixels storage.
* Allows to get pixels of render buffer into a partial slice of the destination. */
int offset = 0;
};
PassAccessor(const PassAccessInfo &pass_access_info,
const float exposure,
const int num_samples);
virtual ~PassAccessor() = default;
/* Get pass data from the given render buffers, perform needed filtering, and store result into
* the pixels.
* The result is stored sequentially starting from the very beginning of the pixels memory. */
bool get_render_tile_pixels(const RenderBuffers *render_buffers,
const Destination &destination) const;
bool get_render_tile_pixels(const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination) const;
/* Set pass data for the given render buffers. Used for baking to read from passes. */
bool set_render_tile_pixels(RenderBuffers *render_buffers, const Source &source);
const PassAccessInfo &get_pass_access_info() const
{
return pass_access_info_;
}
protected:
virtual void init_kernel_film_convert(KernelFilmConvert *kfilm_convert,
const BufferParams &buffer_params,
const Destination &destination) const;
#define DECLARE_PASS_ACCESSOR(pass) \
virtual void get_pass_##pass(const RenderBuffers *render_buffers, \
const BufferParams &buffer_params, \
const Destination &destination) const = 0;
/* Float (scalar) passes. */
DECLARE_PASS_ACCESSOR(depth)
DECLARE_PASS_ACCESSOR(mist)
DECLARE_PASS_ACCESSOR(volume_majorant)
DECLARE_PASS_ACCESSOR(sample_count)
DECLARE_PASS_ACCESSOR(float)
/* Float3 passes. */
DECLARE_PASS_ACCESSOR(light_path)
DECLARE_PASS_ACCESSOR(shadow_catcher)
DECLARE_PASS_ACCESSOR(rgbe)
DECLARE_PASS_ACCESSOR(float3)
/* Float4 passes. */
DECLARE_PASS_ACCESSOR(motion)
DECLARE_PASS_ACCESSOR(cryptomatte)
DECLARE_PASS_ACCESSOR(shadow_catcher_matte_with_shadow)
DECLARE_PASS_ACCESSOR(combined)
DECLARE_PASS_ACCESSOR(float4)
#undef DECLARE_PASS_ACCESSOR
PassAccessInfo pass_access_info_;
float exposure_ = 0.0f;
int num_samples_ = 0;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,127 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "device/device.h"
#include "integrator/pass_accessor_cpu.h"
#include "session/buffers.h"
#include "util/log.h"
#include "util/tbb.h"
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
/* --------------------------------------------------------------------
* Kernel processing.
*/
inline void PassAccessorCPU::run_get_pass_kernel_processor_float(
const KernelFilmConvert *kfilm_convert,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination,
const CPUKernels::FilmConvertFunction func) const
{
/* NOTE: No overlays are applied since they are not used for final renders.
* Can be supported via some sort of specialization to avoid code duplication. */
DCHECK_EQ(destination.stride, 0) << "Custom stride for float destination is not implemented.";
const int64_t pass_stride = buffer_params.pass_stride;
const int64_t buffer_row_stride = buffer_params.stride * buffer_params.pass_stride;
const float *window_data = render_buffers->buffer.data() + buffer_params.window_x * pass_stride +
buffer_params.window_y * buffer_row_stride;
const int pixel_stride = destination.pixel_stride ? destination.pixel_stride :
destination.num_components;
parallel_for(0, buffer_params.window_height, [&](int64_t y) {
const float *buffer = window_data + y * buffer_row_stride;
float *pixel = destination.pixels + destination.pixel_offset +
(y * buffer_params.window_width + destination.offset) * pixel_stride;
func(kfilm_convert, buffer, pixel, buffer_params.window_width, pass_stride, pixel_stride);
});
}
inline void PassAccessorCPU::run_get_pass_kernel_processor_half_rgba(
const KernelFilmConvert *kfilm_convert,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination,
const CPUKernels::FilmConvertHalfRGBAFunction func) const
{
const int64_t pass_stride = buffer_params.pass_stride;
const int64_t buffer_row_stride = buffer_params.stride * buffer_params.pass_stride;
const float *window_data = render_buffers->buffer.data() + buffer_params.window_x * pass_stride +
buffer_params.window_y * buffer_row_stride;
half4 *dst_start = destination.pixels_half_rgba + destination.offset;
const int destination_stride = destination.stride != 0 ? destination.stride :
buffer_params.window_width;
parallel_for(0, buffer_params.window_height, [&](int64_t y) {
const float *buffer = window_data + y * buffer_row_stride;
half4 *pixel = dst_start + y * destination_stride;
func(kfilm_convert, buffer, pixel, buffer_params.window_width, pass_stride);
});
}
/* --------------------------------------------------------------------
* Pass accessors.
*/
#define DEFINE_PASS_ACCESSOR(pass) \
void PassAccessorCPU::get_pass_##pass(const RenderBuffers *render_buffers, \
const BufferParams &buffer_params, \
const Destination &destination) const \
{ \
const CPUKernels &kernels = Device::get_cpu_kernels(); \
KernelFilmConvert kfilm_convert; \
init_kernel_film_convert(&kfilm_convert, buffer_params, destination); \
\
if (destination.pixels) { \
run_get_pass_kernel_processor_float(&kfilm_convert, \
render_buffers, \
buffer_params, \
destination, \
kernels.film_convert_##pass); \
} \
\
if (destination.pixels_half_rgba) { \
run_get_pass_kernel_processor_half_rgba(&kfilm_convert, \
render_buffers, \
buffer_params, \
destination, \
kernels.film_convert_half_rgba_##pass); \
} \
}
/* Float (scalar) passes. */
DEFINE_PASS_ACCESSOR(depth)
DEFINE_PASS_ACCESSOR(mist)
DEFINE_PASS_ACCESSOR(volume_majorant)
DEFINE_PASS_ACCESSOR(sample_count)
DEFINE_PASS_ACCESSOR(float)
/* Float3 passes. */
DEFINE_PASS_ACCESSOR(light_path)
DEFINE_PASS_ACCESSOR(shadow_catcher)
DEFINE_PASS_ACCESSOR(rgbe)
DEFINE_PASS_ACCESSOR(float3)
/* Float4 passes. */
DEFINE_PASS_ACCESSOR(motion)
DEFINE_PASS_ACCESSOR(cryptomatte)
DEFINE_PASS_ACCESSOR(shadow_catcher_matte_with_shadow)
DEFINE_PASS_ACCESSOR(combined)
DEFINE_PASS_ACCESSOR(float4)
#undef DEFINE_PASS_ACCESSOR
CCL_NAMESPACE_END

View File

@@ -0,0 +1,63 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "device/cpu/kernel.h"
#include "integrator/pass_accessor.h"
CCL_NAMESPACE_BEGIN
struct KernelFilmConvert;
/* Pass accessor implementation for CPU side. */
class PassAccessorCPU : public PassAccessor {
public:
using PassAccessor::PassAccessor;
protected:
inline void run_get_pass_kernel_processor_float(
const KernelFilmConvert *kfilm_convert,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination,
const CPUKernels::FilmConvertFunction func) const;
inline void run_get_pass_kernel_processor_half_rgba(
const KernelFilmConvert *kfilm_convert,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination,
const CPUKernels::FilmConvertHalfRGBAFunction func) const;
#define DECLARE_PASS_ACCESSOR(pass) \
virtual void get_pass_##pass(const RenderBuffers *render_buffers, \
const BufferParams &buffer_params, \
const Destination &destination) const override;
/* Float (scalar) passes. */
DECLARE_PASS_ACCESSOR(depth)
DECLARE_PASS_ACCESSOR(mist)
DECLARE_PASS_ACCESSOR(volume_majorant)
DECLARE_PASS_ACCESSOR(sample_count)
DECLARE_PASS_ACCESSOR(float)
/* Float3 passes. */
DECLARE_PASS_ACCESSOR(light_path)
DECLARE_PASS_ACCESSOR(shadow_catcher)
DECLARE_PASS_ACCESSOR(rgbe)
DECLARE_PASS_ACCESSOR(float3)
/* Float4 passes. */
DECLARE_PASS_ACCESSOR(motion)
DECLARE_PASS_ACCESSOR(cryptomatte)
DECLARE_PASS_ACCESSOR(shadow_catcher_matte_with_shadow)
DECLARE_PASS_ACCESSOR(combined)
DECLARE_PASS_ACCESSOR(float4)
#undef DECLARE_PASS_ACCESSOR
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "device/queue.h"
#include "integrator/pass_accessor_gpu.h"
#include "session/buffers.h"
CCL_NAMESPACE_BEGIN
PassAccessorGPU::PassAccessorGPU(DeviceQueue *queue,
const PassAccessInfo &pass_access_info,
const float exposure,
const int num_samples)
: PassAccessor(pass_access_info, exposure, num_samples), queue_(queue)
{
}
/* --------------------------------------------------------------------
* Kernel execution.
*/
void PassAccessorGPU::run_film_convert_kernels(DeviceKernel kernel,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination) const
{
KernelFilmConvert kfilm_convert;
init_kernel_film_convert(&kfilm_convert, buffer_params, destination);
const int work_size = buffer_params.window_width * buffer_params.window_height;
const int destination_stride = destination.stride != 0 ? destination.stride :
buffer_params.window_width;
const int offset = buffer_params.window_x * buffer_params.pass_stride +
buffer_params.window_y * buffer_params.stride * buffer_params.pass_stride;
queue_->init_execution();
if (destination.d_pixels) {
DCHECK_EQ(destination.stride, 0) << "Custom stride for float destination is not implemented.";
const DeviceKernelArguments args(&kfilm_convert,
&destination.d_pixels,
&render_buffers->buffer.device_pointer,
&work_size,
&buffer_params.window_width,
&offset,
&buffer_params.stride,
&destination.pixel_offset,
&destination.offset,
&destination_stride);
queue_->enqueue(kernel, work_size, args);
}
if (destination.d_pixels_half_rgba) {
const DeviceKernel kernel_half_float = static_cast<DeviceKernel>(kernel + 1);
const DeviceKernelArguments args(&kfilm_convert,
&destination.d_pixels_half_rgba,
&render_buffers->buffer.device_pointer,
&work_size,
&buffer_params.window_width,
&offset,
&buffer_params.stride,
&destination.offset,
&destination_stride);
queue_->enqueue(kernel_half_float, work_size, args);
}
queue_->synchronize();
}
/* --------------------------------------------------------------------
* Pass accessors.
*/
#define DEFINE_PASS_ACCESSOR(pass, kernel_pass) \
void PassAccessorGPU::get_pass_##pass(const RenderBuffers *render_buffers, \
const BufferParams &buffer_params, \
const Destination &destination) const \
{ \
run_film_convert_kernels( \
DEVICE_KERNEL_FILM_CONVERT_##kernel_pass, render_buffers, buffer_params, destination); \
}
/* Float (scalar) passes. */
DEFINE_PASS_ACCESSOR(depth, DEPTH);
DEFINE_PASS_ACCESSOR(mist, MIST);
DEFINE_PASS_ACCESSOR(volume_majorant, VOLUME_MAJORANT);
DEFINE_PASS_ACCESSOR(sample_count, SAMPLE_COUNT);
DEFINE_PASS_ACCESSOR(float, FLOAT);
/* Float3 passes. */
DEFINE_PASS_ACCESSOR(light_path, LIGHT_PATH);
DEFINE_PASS_ACCESSOR(rgbe, RGBE);
DEFINE_PASS_ACCESSOR(float3, FLOAT3);
/* Float4 passes. */
DEFINE_PASS_ACCESSOR(motion, MOTION);
DEFINE_PASS_ACCESSOR(cryptomatte, CRYPTOMATTE);
DEFINE_PASS_ACCESSOR(shadow_catcher, SHADOW_CATCHER);
DEFINE_PASS_ACCESSOR(shadow_catcher_matte_with_shadow, SHADOW_CATCHER_MATTE_WITH_SHADOW);
DEFINE_PASS_ACCESSOR(combined, COMBINED);
DEFINE_PASS_ACCESSOR(float4, FLOAT4);
#undef DEFINE_PASS_ACCESSOR
CCL_NAMESPACE_END

View File

@@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/pass_accessor.h"
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
class DeviceQueue;
/* Pass accessor implementation for GPU side. */
class PassAccessorGPU : public PassAccessor {
public:
PassAccessorGPU(DeviceQueue *queue,
const PassAccessInfo &pass_access_info,
const float exposure,
int num_samples);
protected:
void run_film_convert_kernels(DeviceKernel kernel,
const RenderBuffers *render_buffers,
const BufferParams &buffer_params,
const Destination &destination) const;
#define DECLARE_PASS_ACCESSOR(pass) \
virtual void get_pass_##pass(const RenderBuffers *render_buffers, \
const BufferParams &buffer_params, \
const Destination &destination) const override;
/* Float (scalar) passes. */
DECLARE_PASS_ACCESSOR(depth);
DECLARE_PASS_ACCESSOR(mist);
DECLARE_PASS_ACCESSOR(volume_majorant);
DECLARE_PASS_ACCESSOR(sample_count);
DECLARE_PASS_ACCESSOR(float);
/* Float3 passes. */
DECLARE_PASS_ACCESSOR(light_path);
DECLARE_PASS_ACCESSOR(rgbe);
DECLARE_PASS_ACCESSOR(float3);
/* Float4 passes. */
DECLARE_PASS_ACCESSOR(motion);
DECLARE_PASS_ACCESSOR(cryptomatte);
DECLARE_PASS_ACCESSOR(shadow_catcher);
DECLARE_PASS_ACCESSOR(shadow_catcher_matte_with_shadow);
DECLARE_PASS_ACCESSOR(combined);
DECLARE_PASS_ACCESSOR(float4);
#undef DECLARE_PASS_ACCESSOR
DeviceQueue *queue_;
};
CCL_NAMESPACE_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,366 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <functional>
#include "integrator/denoiser.h"
#include "integrator/guiding.h"
#include "integrator/pass_accessor.h"
#include "integrator/path_trace_work.h"
#include "integrator/work_balancer.h"
#include "session/buffers.h"
#include "util/guiding.h" // IWYU pragma: keep
#include "util/thread.h"
#include "util/unique_ptr.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
class AdaptiveSampling;
class Device;
class DeviceScene;
class DisplayDriver;
class Film;
class RenderBuffers;
class RenderScheduler;
class RenderWork;
class PathTraceDisplay;
class OutputDriver;
class Progress;
class TileManager;
/* PathTrace class takes care of kernel graph and scheduling on a (multi)device. It takes care of
* all the common steps of path tracing which are not device-specific. The list of tasks includes
* but is not limited to:
* - Kernel graph.
* - Scheduling logic.
* - Queues management.
* - Adaptive stopping. */
class PathTrace {
public:
/* Render scheduler is used to report timing information and access things like start/finish
* sample. */
PathTrace(Device *device,
Device *denoise_device,
Film *film,
DeviceScene *device_scene,
RenderScheduler &render_scheduler,
TileManager &tile_manager);
~PathTrace();
/* Create devices and load kernels which are created on-demand (for example, denoising devices).
* The progress is reported to the currently configure progress object (via `set_progress`). */
void load_kernels();
/* Allocate working memory. This runs before allocating scene memory so that we can estimate
* more accurately which scene device memory may need to allocated on the host. */
void alloc_work_memory();
/* Check whether now it is a good time to reset rendering.
* Used to avoid very often resets in the viewport, giving it a chance to draw intermediate
* render result. */
bool ready_to_reset();
void reset(const BufferParams &full_params,
const BufferParams &big_tile_params,
bool reset_rendering);
void device_free();
/* Set progress tracker.
* Used to communicate details about the progress to the outer world, check whether rendering is
* to be canceled.
*
* The path tracer writes to this object, and then at a convenient moment runs
* progress_update_cb() callback. */
void set_progress(Progress *progress);
/* NOTE: This is a blocking call. Meaning, it will not return until given number of samples are
* rendered (or until rendering is requested to be canceled). */
void render(const RenderWork &render_work);
/* TODO(sergey): Decide whether denoiser is really a part of path tracer. Currently it is
* convenient to have it here because then its easy to access render buffer. But the downside is
* that this adds too much of entities which can live separately with some clear API. */
/* Set denoiser parameters.
* Use this to configure the denoiser before rendering any samples. */
void set_denoiser_params(const DenoiseParams &params);
/* Set parameters used for adaptive sampling.
* Use this to configure the adaptive sampler before rendering any samples. */
void set_adaptive_sampling(const AdaptiveSampling &adaptive_sampling);
/* Set the parameters for guiding.
* Use to setup the guiding structures before each rendering iteration. */
void set_guiding_params(const GuidingParams &params, const bool reset);
/* Sets output driver for render buffer output. */
void set_output_driver(unique_ptr<OutputDriver> driver);
/* Set display driver for interactive render buffer display. */
void set_display_driver(unique_ptr<DisplayDriver> driver);
/* Clear the display buffer by filling it in with all zeroes. */
void zero_display();
/* Perform drawing of the current state of the DisplayDriver. */
void draw();
/* Flush outstanding display commands before ending the render loop. */
void flush_display();
/* Cancel rendering process as soon as possible, without waiting for full tile to be sampled.
* Used in cases like reset of render session.
*
* This is a blocking call, which returns as soon as there is no running `render_samples()` call.
*/
void cancel();
/* Copy an entire render buffer to/from the path trace. */
/* Copy happens via CPU side buffer: data will be copied from every device of the path trace, and
* the data will be copied to the device of the given render buffers. */
void copy_to_render_buffers(RenderBuffers *render_buffers);
/* Copy happens via CPU side buffer: data will be copied from the device of the given render
* buffers and will be copied to all devices of the path trace. */
void copy_from_render_buffers(RenderBuffers *render_buffers);
/* Copy render buffers of the big tile from the device to host.
* Return true if all copies are successful. */
bool copy_render_tile_from_device();
/* 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);
/* Get number of samples in the current big tile render buffers. */
int get_num_render_tile_samples() const;
/* Get pass data of the entire big tile.
* This call puts pass render result from all devices into the final pixels storage.
*
* NOTE: Expects buffers to be copied to the host using `copy_render_tile_from_device()`.
*
* Returns false if any of the accessor's `get_render_tile_pixels()` returned false. */
bool get_render_tile_pixels(const PassAccessor &pass_accessor,
const PassAccessor::Destination &destination);
/* Set pass data for baking. */
bool set_render_tile_pixels(PassAccessor &pass_accessor, const PassAccessor::Source &source);
/* Check whether denoiser was run and denoised passes are available. */
bool has_denoised_result() const;
/* Get size and offset (relative to the buffer's full x/y) of the currently rendering tile.
* In the case of tiled rendering this will return full-frame after all tiles has been rendered.
*
* NOTE: If the full-frame buffer processing is in progress, returns parameters of the full-frame
* instead. */
int2 get_render_tile_size() const;
int2 get_render_tile_offset() const;
int2 get_render_size() const;
/* Get buffer parameters of the current tile.
*
* NOTE: If the full-frame buffer processing is in progress, returns parameters of the full-frame
* instead. */
const BufferParams &get_render_tile_params() const;
/* Generate full multi-line report of the rendering process, including rendering parameters,
* times, and so on. */
string full_report() const;
/* Callback which is called to report current rendering progress.
*
* It is supposed to be cheaper than buffer update/write, hence can be called more often.
* Additionally, it might be called form the middle of wavefront (meaning, it is not guaranteed
* that the buffer is "uniformly" sampled at the moment of this callback). */
std::function<void(void)> progress_update_cb;
protected:
/* Actual implementation of the rendering pipeline.
* Calls steps in order, checking for the cancel to be requested in between.
*
* Is separate from `render()` to simplify dealing with the early outputs and keeping
* `render_cancel_` in the consistent state. */
void render_pipeline(RenderWork render_work);
/* Initialize kernel execution on all integrator queues. */
void render_init_kernel_execution();
/* Release kernel execution resources on all integrator queues. */
void render_deinit_kernel_execution();
/* Make sure both allocated and effective buffer parameters of path tracer works are up to date
* with the current big tile parameters, performance-dependent slicing, and resolution divider.
*/
void update_work_buffer_params_if_needed(const RenderWork &render_work);
void update_allocated_work_buffer_params();
void update_effective_work_buffer_params(const RenderWork &render_work);
/* Perform various steps of the render work.
*
* Note that some steps might modify the work, forcing some steps to happen within this iteration
* of rendering. */
void init_render_buffers(const RenderWork &render_work);
void path_trace(RenderWork &render_work);
void adaptive_sample(RenderWork &render_work);
void denoise(const RenderWork &render_work);
void denoise_volume_guiding_buffers(const RenderWork &render_work, const bool has_volume);
void cryptomatte_postprocess(const RenderWork &render_work);
void update_display(const RenderWork &render_work);
void rebalance(const RenderWork &render_work);
void write_tile_buffer(const RenderWork &render_work);
void finalize_full_buffer_on_disk(const RenderWork &render_work);
/* Updates/initializes the guiding structures after a rendering iteration.
* The structures are updated using the training data/samples generated during the previous
* rendering iteration */
void guiding_update_structures();
/* Prepares the per-kernel thread related guiding structures (e.g., PathSegmentStorage,
* pointers to the global Field and SegmentStorage)*/
void guiding_prepare_structures();
/* Get number of samples in the current state of the render buffers. */
int get_num_samples_in_buffer();
/* Check whether user requested to cancel rendering, so that path tracing is to be finished as
* soon as possible. */
bool is_cancel_requested();
/* Write the big tile render buffer via the write callback. */
void tile_buffer_write();
/* Read the big tile render buffer via the read callback. */
void tile_buffer_read();
/* Write current tile into the file on disk. */
void tile_buffer_write_to_disk();
/* Run the progress_update_cb callback if it is needed. */
void progress_update_if_needed(const RenderWork &render_work);
void progress_set_status(const string &status, const string &substatus = "");
/* Destroy GPU resources (such as graphics interop) used by work. */
void destroy_gpu_resources();
/* Pointer to a device which is configured to be used for path tracing. If multiple devices
* are configured this is a `MultiDevice`. */
Device *device_ = nullptr;
/* Pointer to a device which is configured to be used for denoising. Can be identical
* to the device */
Device *denoise_device_ = nullptr;
/* CPU device for creating temporary render buffers on the CPU side. */
unique_ptr<Device> cpu_device_;
Film *film_;
DeviceScene *device_scene_;
RenderScheduler &render_scheduler_;
TileManager &tile_manager_;
/* Display driver for interactive render buffer display. */
unique_ptr<PathTraceDisplay> display_;
/* Output driver to write render buffer to. */
unique_ptr<OutputDriver> output_driver_;
/* Per-compute device descriptors of work which is responsible for path tracing on its configured
* device. */
vector<unique_ptr<PathTraceWork>> path_trace_works_;
/* Per-path trace work information needed for multi-device balancing. */
vector<WorkBalanceInfo> work_balance_infos_;
/* Render buffer parameters of the full frame and current big tile. */
BufferParams full_params_;
BufferParams big_tile_params_;
/* Denoiser which takes care of denoising the big tile. */
unique_ptr<Denoiser> denoiser_;
/* Denoiser device descriptor which holds the denoised big tile for multi-device workloads. */
unique_ptr<PathTraceWork> big_tile_denoise_work_;
#if defined(WITH_PATH_GUIDING)
/* Guiding related attributes */
GuidingParams guiding_params_;
/* The guiding field which holds the representation of the incident radiance field for the
* complete scene. */
unique_ptr<openpgl::cpp::Field> guiding_field_;
/* The storage container which holds the training data/samples generated during the last
* rendering iteration. */
unique_ptr<openpgl::cpp::SampleStorage> guiding_sample_data_storage_;
/* The number of already performed training iterations for the guiding field. */
int guiding_update_count = 0;
#endif
/* State which is common for all the steps of the render work.
* Is brought up to date in the `render()` call and is accessed from all the steps involved into
* rendering the work. */
struct {
/* Denotes whether render buffers parameters of path trace works are to be reset for the new
* value of the big tile parameters. */
bool need_reset_params = false;
/* Divider of the resolution for faster previews.
*
* Allows to re-use same render buffer, but have less pixels rendered into in it. The way to
* think of render buffer in this case is as an over-allocated array: the resolution divider
* affects both resolution and stride as visible by the integrator kernels. */
float resolution_divider = 0;
/* Parameters of the big tile with the current resolution divider applied. */
BufferParams effective_big_tile_params;
BufferParams effective_denoised_big_tile_params;
/* Denoiser was run and there are denoised versions of the passes in the render buffers. */
bool has_denoised_result = false;
/* Current tile has been written (to either disk or callback.
* Indicates that no more work will be done on this tile. */
bool tile_written = false;
} render_state_;
/* Progress object which is used to communicate sample progress. */
Progress *progress_;
/* Fields required for canceling render on demand, as quickly as possible. */
struct {
/* Indicates whether there is an on-going `render_samples()` call. */
bool is_rendering = false;
/* Indicates whether rendering is requested to be canceled by `cancel()`. */
bool is_requested = false;
/* Synchronization between thread which does `render_samples()` and thread which does
* `cancel()`. */
thread_mutex mutex;
thread_condition_variable condition;
} render_cancel_;
/* Indicates whether a render result was drawn after latest session reset.
* Used by `ready_to_reset()` to implement logic which feels the most interactive. */
bool did_draw_after_reset_ = true;
/* State of the full frame processing and writing to the software. */
struct {
RenderBuffers *render_buffers = nullptr;
} full_frame_state_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,264 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/path_trace_display.h"
#include "session/buffers.h"
#include "session/display_driver.h"
#include "util/log.h"
CCL_NAMESPACE_BEGIN
PathTraceDisplay::PathTraceDisplay(unique_ptr<DisplayDriver> driver) : driver_(std::move(driver))
{
}
void PathTraceDisplay::reset(const BufferParams &buffer_params, const bool reset_rendering)
{
const thread_scoped_lock lock(mutex_);
params_.full_offset = make_int2(buffer_params.full_x + buffer_params.window_x,
buffer_params.full_y + buffer_params.window_y);
params_.full_size = make_int2(buffer_params.full_width, buffer_params.full_height);
params_.size = make_int2(buffer_params.window_width, buffer_params.window_height);
texture_state_.is_outdated = true;
if (!reset_rendering) {
driver_->next_tile_begin();
}
}
void PathTraceDisplay::mark_texture_updated()
{
texture_state_.is_outdated = false;
}
/* --------------------------------------------------------------------
* Update procedure.
*/
bool PathTraceDisplay::update_begin(const int texture_width, const int texture_height)
{
DCHECK(!update_state_.is_active);
if (update_state_.is_active) {
LOG_ERROR << "Attempt to re-activate update process.";
return false;
}
/* Get parameters within a mutex lock, to avoid reset() modifying them at the same time.
* The update itself is non-blocking however, for better performance and to avoid
* potential deadlocks due to locks held by the subclass. */
DisplayDriver::Params params;
{
const thread_scoped_lock lock(mutex_);
params = params_;
texture_state_.size = make_int2(texture_width, texture_height);
}
if (!driver_->update_begin(params, texture_width, texture_height)) {
LOG_ERROR << "PathTraceDisplay implementation could not begin update.";
return false;
}
update_state_.is_active = true;
return true;
}
void PathTraceDisplay::update_end()
{
DCHECK(update_state_.is_active);
if (!update_state_.is_active) {
LOG_ERROR << "Attempt to deactivate inactive update process.";
return;
}
driver_->update_end();
update_state_.is_active = false;
}
int2 PathTraceDisplay::get_texture_size() const
{
return texture_state_.size;
}
/* --------------------------------------------------------------------
* Texture update from CPU buffer.
*/
void PathTraceDisplay::copy_pixels_to_texture(const half4 *rgba_pixels,
const int texture_x,
const int texture_y,
const int pixels_width,
const int pixels_height)
{
DCHECK(update_state_.is_active);
if (!update_state_.is_active) {
LOG_ERROR << "Attempt to copy pixels data outside of PathTraceDisplay update.";
return;
}
mark_texture_updated();
/* This call copies pixels to a mapped texture buffer which is typically much cheaper from CPU
* time point of view than to copy data directly to a texture.
*
* The possible downside of this approach is that it might require a higher peak memory when
* doing partial updates of the texture (although, in practice even partial updates might peak
* with a full-frame buffer stored on the CPU if the GPU is currently occupied). */
half4 *mapped_rgba_pixels = map_texture_buffer();
if (!mapped_rgba_pixels) {
return;
}
const int texture_width = texture_state_.size.x;
const int texture_height = texture_state_.size.y;
if (texture_x == 0 && texture_y == 0 && pixels_width == texture_width &&
pixels_height == texture_height)
{
const size_t size_in_bytes = sizeof(half4) * texture_width * texture_height;
memcpy(mapped_rgba_pixels, rgba_pixels, size_in_bytes);
}
else {
const half4 *rgba_row = rgba_pixels;
half4 *mapped_rgba_row = mapped_rgba_pixels + texture_y * texture_width + texture_x;
for (int y = 0; y < pixels_height;
++y, rgba_row += pixels_width, mapped_rgba_row += texture_width)
{
memcpy(mapped_rgba_row, rgba_row, sizeof(half4) * pixels_width);
}
}
unmap_texture_buffer();
}
/* --------------------------------------------------------------------
* Texture buffer mapping.
*/
half4 *PathTraceDisplay::map_texture_buffer()
{
DCHECK(!texture_buffer_state_.is_mapped);
DCHECK(update_state_.is_active);
if (texture_buffer_state_.is_mapped) {
LOG_ERROR << "Attempt to re-map an already mapped texture buffer.";
return nullptr;
}
if (!update_state_.is_active) {
LOG_ERROR << "Attempt to copy pixels data outside of PathTraceDisplay update.";
return nullptr;
}
half4 *mapped_rgba_pixels = driver_->map_texture_buffer();
if (mapped_rgba_pixels) {
texture_buffer_state_.is_mapped = true;
}
return mapped_rgba_pixels;
}
void PathTraceDisplay::unmap_texture_buffer()
{
DCHECK(texture_buffer_state_.is_mapped);
if (!texture_buffer_state_.is_mapped) {
LOG_ERROR << "Attempt to unmap non-mapped texture buffer.";
return;
}
texture_buffer_state_.is_mapped = false;
mark_texture_updated();
driver_->unmap_texture_buffer();
}
/* --------------------------------------------------------------------
* Graphics interoperability.
*/
GraphicsInteropDevice PathTraceDisplay::graphics_interop_get_device()
{
return driver_->graphics_interop_get_device();
}
GraphicsInteropBuffer &PathTraceDisplay::graphics_interop_get_buffer()
{
GraphicsInteropBuffer &interop_buffer = driver_->graphics_interop_get_buffer();
DCHECK(!texture_buffer_state_.is_mapped);
DCHECK(update_state_.is_active);
if (texture_buffer_state_.is_mapped) {
LOG_ERROR
<< "Attempt to use graphics interoperability mode while the texture buffer is mapped.";
interop_buffer.clear();
return interop_buffer;
}
if (!update_state_.is_active) {
LOG_ERROR << "Attempt to use graphics interoperability outside of PathTraceDisplay update.";
interop_buffer.clear();
return interop_buffer;
}
/* Assume that interop will write new values to the texture. */
mark_texture_updated();
driver_->graphics_interop_update_buffer();
return interop_buffer;
}
void PathTraceDisplay::graphics_interop_activate()
{
driver_->graphics_interop_activate();
}
void PathTraceDisplay::graphics_interop_deactivate()
{
driver_->graphics_interop_deactivate();
}
/* --------------------------------------------------------------------
* Drawing.
*/
void PathTraceDisplay::zero()
{
driver_->zero();
}
bool PathTraceDisplay::draw()
{
/* Get parameters within a mutex lock, to avoid reset() modifying them at the same time.
* The drawing itself is non-blocking however, for better performance and to avoid
* potential deadlocks due to locks held by the subclass. */
DisplayDriver::Params params;
bool is_outdated;
{
const thread_scoped_lock lock(mutex_);
params = params_;
is_outdated = texture_state_.is_outdated;
}
driver_->draw(params);
return !is_outdated;
}
void PathTraceDisplay::flush()
{
driver_->flush();
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,189 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "session/display_driver.h"
#include "util/half.h"
#include "util/thread.h"
#include "util/unique_ptr.h"
CCL_NAMESPACE_BEGIN
class BufferParams;
/* PathTraceDisplay is used for efficient render buffer display.
*
* The host applications implements a DisplayDriver, storing a render pass in a GPU-side
* textures. This texture is continuously updated by the path tracer and drawn by the host
* application.
*
* PathTraceDisplay is a wrapper around the DisplayDriver, adding thread safety, state tracking
* and error checking. */
class PathTraceDisplay {
public:
explicit PathTraceDisplay(unique_ptr<DisplayDriver> driver);
virtual ~PathTraceDisplay() = default;
/* Reset the display for the new state of render session. Is called whenever session is reset,
* which happens on changes like viewport navigation or viewport dimension change.
*
* This call will configure parameters for a changed buffer and reset the texture state.
*
* When the `reset_rendering` a complete display reset happens. When it is false reset happens
* for a new state of the buffer parameters which is assumed to correspond to the next tile. */
void reset(const BufferParams &buffer_params, bool reset_rendering);
/* --------------------------------------------------------------------
* Update procedure.
*
* These calls indicates a desire of the caller to update content of the displayed texture. */
/* Returns true when update is ready. Update should be finished with update_end().
*
* If false is returned then no update is possible, and no update_end() call is needed.
*
* The texture width and height denotes an actual resolution of the underlying render result. */
bool update_begin(const int texture_width, const int texture_height);
void update_end();
/* Get currently configured texture size of the display (as configured by `update_begin()`. */
int2 get_texture_size() const;
/* --------------------------------------------------------------------
* Texture update from CPU buffer.
*
* NOTE: The PathTraceDisplay should be marked for an update being in process with
* `update_begin()`.
*
* Most portable implementation, which must be supported by all platforms. Might not be the most
* efficient one.
*/
/* Copy buffer of rendered pixels of a given size into a given position of the texture.
*
* This function does not acquire a lock. The reason for this is to allow use of this function
* for partial updates from different devices. In this case the caller will acquire the lock
* once, update all the slices and release
* the lock once. This will ensure that draw() will never use partially updated texture. */
void copy_pixels_to_texture(const half4 *rgba_pixels,
const int texture_x,
const int texture_y,
const int pixels_width,
const int pixels_height);
/* --------------------------------------------------------------------
* Texture buffer mapping.
*
* This functionality is used to update GPU-side texture content without need to maintain CPU
* side buffer on the caller.
*
* NOTE: The PathTraceDisplay should be marked for an update being in process with
* `update_begin()`.
*
* NOTE: Texture buffer can not be mapped while graphics interoperability is active. This means
* that `map_texture_buffer()` is not allowed between `graphics_interop_begin()` and
* `graphics_interop_end()` calls.
*/
/* Map pixels memory form texture to a buffer available for write from CPU. Width and height will
* define a requested size of the texture to write to.
* Upon success a non-null pointer is returned and the texture buffer is to be unmapped.
* If an error happens during mapping, or if mapping is not supported by this GPU display a
* null pointer is returned and the buffer is NOT to be unmapped.
*
* NOTE: Usually the implementation will rely on a GPU context of some sort, and the GPU context
* is often can not be bound to two threads simultaneously, and can not be released from a
* different thread. This means that the mapping API should be used from the single thread only,
*/
half4 *map_texture_buffer();
void unmap_texture_buffer();
/* --------------------------------------------------------------------
* Graphics interoperability.
*
* A special code path which allows to update texture content directly from the GPU compute
* device. Complementary part of DeviceGraphicsInterop.
*
* NOTE: Graphics interoperability can not be used while the texture buffer is mapped. This means
* that `graphics_interop_get_buffer()` is not allowed between `map_texture_buffer()` and
* `unmap_texture_buffer()` calls. */
/* Get PathTraceDisplay graphics interoperability information which acts as a destination for the
* device API. */
GraphicsInteropDevice graphics_interop_get_device();
GraphicsInteropBuffer &graphics_interop_get_buffer();
/* (De)activate GPU display for graphics interoperability outside of regular display update
* routines. */
void graphics_interop_activate();
void graphics_interop_deactivate();
/* --------------------------------------------------------------------
* Drawing.
*/
/* Clear the texture by filling it with all zeroes.
*
* This call might happen in parallel with draw, but can never happen in parallel with the
* update.
*
* The actual zeroing can be deferred to a later moment. What is important is that after clear
* and before pixels update the drawing texture will be fully empty, and that partial update
* after clear will write new pixel values for an updating area, leaving everything else zeroed.
*
* If the GPU display supports graphics interoperability then the zeroing the display is to be
* delegated to the device via the `GraphicsInterop`. */
void zero();
/* Draw the current state of the texture.
*
* Returns true if this call did draw an updated state of the texture. */
bool draw();
/* Flush outstanding display commands before ending the render loop. */
void flush();
private:
/* Display driver implemented by the host application. */
unique_ptr<DisplayDriver> driver_;
/* Current display parameters */
thread_mutex mutex_;
DisplayDriver::Params params_;
/* Mark texture as its content has been updated.
* Used from places which knows that the texture content has been brought up-to-date, so that the
* drawing knows whether it can be performed, and whether drawing happened with an up-to-date
* texture state. */
void mark_texture_updated();
/* State of the update process. */
struct {
/* True when update is in process, indicated by `update_begin()` / `update_end()`. */
bool is_active = false;
} update_state_;
/* State of the texture, which is needed for an integration with render session and interactive
* updates and navigation. */
struct {
/* Texture is considered outdated after `reset()` until the next call of
* `copy_pixels_to_texture()`. */
bool is_outdated = true;
/* Texture size in pixels. */
int2 size = make_int2(0, 0);
} texture_state_;
/* State of the texture buffer. Is tracked to perform sanity checks. */
struct {
/* True when the texture buffer is mapped with `map_texture_buffer()`. */
bool is_mapped = false;
} texture_buffer_state_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,103 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/path_trace_tile.h"
#include "integrator/pass_accessor_cpu.h"
#include "integrator/path_trace.h"
#include "scene/pass.h"
#include "session/buffers.h"
CCL_NAMESPACE_BEGIN
PathTraceTile::PathTraceTile(PathTrace &path_trace)
: OutputDriver::Tile(path_trace.get_render_tile_offset(),
path_trace.get_render_tile_size(),
path_trace.get_render_size(),
path_trace.get_render_tile_params().layer,
path_trace.get_render_tile_params().view),
path_trace_(path_trace),
copied_from_device_(false)
{
}
bool PathTraceTile::get_pass_pixels(const string_view pass_name,
const int num_channels,
float *pixels) const
{
/* NOTE: The code relies on a fact that session is fully update and no scene/buffer modification
* is happening while this function runs. */
if (!copied_from_device_) {
/* Copy from device on demand. */
path_trace_.copy_render_tile_from_device();
copied_from_device_ = true;
}
const BufferParams &buffer_params = path_trace_.get_render_tile_params();
const BufferPass *pass = buffer_params.find_pass(pass_name);
if (pass == nullptr) {
return false;
}
const bool has_denoised_result = path_trace_.has_denoised_result() ||
is_volume_guiding_pass(pass->type);
if (pass->mode == PassMode::DENOISED && !has_denoised_result) {
pass = buffer_params.find_pass(pass->type);
if (pass == nullptr) {
/* Happens when denoised result pass is requested but is never written by the kernel. */
return false;
}
}
pass = buffer_params.get_actual_display_pass(pass);
if (pass == nullptr) {
/* Happens when interactive session changes display pass but render
* buffer does not contain it yet. */
return false;
}
const float exposure = buffer_params.exposure;
const int num_samples = path_trace_.get_num_render_tile_samples();
PassAccessor::PassAccessInfo pass_access_info(*pass);
pass_access_info.use_approximate_shadow_catcher = buffer_params.use_approximate_shadow_catcher;
pass_access_info.use_approximate_shadow_catcher_background =
pass_access_info.use_approximate_shadow_catcher && !buffer_params.use_transparent_background;
const PassAccessorCPU pass_accessor(pass_access_info, exposure, num_samples);
const PassAccessor::Destination destination(pixels, num_channels);
return path_trace_.get_render_tile_pixels(pass_accessor, destination);
}
bool PathTraceTile::set_pass_pixels(const string_view pass_name,
const int num_channels,
const float *pixels) const
{
/* NOTE: The code relies on a fact that session is fully update and no scene/buffer modification
* is happening while this function runs. */
const BufferParams &buffer_params = path_trace_.get_render_tile_params();
const BufferPass *pass = buffer_params.find_pass(pass_name);
if (!pass) {
return false;
}
if (pass->offset == PASS_UNUSED) {
/* Happens when attempting to set pixels of a pass with compositing when baking. */
return false;
}
const float exposure = buffer_params.exposure;
const int num_samples = 1;
const PassAccessor::PassAccessInfo pass_access_info(*pass);
PassAccessorCPU pass_accessor(pass_access_info, exposure, num_samples);
const PassAccessor::Source source(pixels, num_channels);
return path_trace_.set_render_tile_pixels(pass_accessor, source);
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2021-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "session/output_driver.h"
CCL_NAMESPACE_BEGIN
/* PathTraceTile
*
* Implementation of OutputDriver::Tile interface for path tracer. */
class PathTrace;
class PathTraceTile : public OutputDriver::Tile {
public:
PathTraceTile(PathTrace &path_trace);
bool get_pass_pixels(const string_view pass_name,
const int num_channels,
float *pixels) const override;
bool set_pass_pixels(const string_view pass_name,
const int num_channels,
const float *pixels) const override;
private:
PathTrace &path_trace_;
mutable bool copied_from_device_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,230 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "device/device.h"
#include "integrator/path_trace_display.h"
#include "integrator/path_trace_work.h"
#include "integrator/path_trace_work_cpu.h"
#include "integrator/path_trace_work_gpu.h"
#include "scene/film.h"
#include "scene/scene.h"
#include "session/buffers.h"
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
unique_ptr<PathTraceWork> PathTraceWork::create(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag)
{
if (device->info.type == DEVICE_CPU) {
return make_unique<PathTraceWorkCPU>(device, film, device_scene, cancel_requested_flag);
}
if (device->info.type == DEVICE_DUMMY) {
/* Dummy devices can't perform any work. */
return nullptr;
}
return make_unique<PathTraceWorkGPU>(device, film, device_scene, cancel_requested_flag);
}
PathTraceWork::PathTraceWork(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag)
: device_(device),
film_(film),
device_scene_(device_scene),
buffers_(make_unique<RenderBuffers>(device)),
effective_buffer_params_(buffers_->params),
effective_denoised_buffer_params_(buffers_->params),
cancel_requested_flag_(cancel_requested_flag)
{
}
PathTraceWork::~PathTraceWork() = default;
RenderBuffers *PathTraceWork::get_render_buffers()
{
return buffers_.get();
}
void PathTraceWork::set_effective_buffer_params(
const BufferParams &effective_big_tile_params,
const BufferParams &effective_buffer_params,
const BufferParams &effective_denoised_big_tile_params,
const BufferParams &effective_denoised_buffer_params)
{
effective_big_tile_params_ = effective_big_tile_params;
effective_buffer_params_ = effective_buffer_params;
effective_denoised_big_tile_params_ = effective_denoised_big_tile_params;
effective_denoised_buffer_params_ = effective_denoised_buffer_params;
}
bool PathTraceWork::has_multiple_works() const
{
/* Assume if there are multiple works working on the same big tile none of the works gets the
* entire big tile to work on. */
return !(effective_big_tile_params_.width == effective_buffer_params_.width &&
effective_big_tile_params_.height == effective_buffer_params_.height &&
effective_big_tile_params_.full_x == effective_buffer_params_.full_x &&
effective_big_tile_params_.full_y == effective_buffer_params_.full_y);
}
void PathTraceWork::copy_to_render_buffers(RenderBuffers *render_buffers)
{
copy_render_buffers_from_device();
const int64_t width = effective_buffer_params_.width;
const int64_t height = effective_buffer_params_.height;
const int64_t pass_stride = effective_buffer_params_.pass_stride;
const int64_t row_stride = width * pass_stride;
const int64_t data_size = row_stride * height * sizeof(float);
const int64_t offset_y = effective_buffer_params_.full_y - effective_big_tile_params_.full_y;
const int64_t offset_in_floats = offset_y * row_stride;
const float *src = buffers_->buffer.data();
float *dst = render_buffers->buffer.data() + offset_in_floats;
memcpy(dst, src, data_size);
}
void PathTraceWork::copy_from_render_buffers(const RenderBuffers *render_buffers)
{
const int64_t width = effective_buffer_params_.width;
const int64_t height = effective_buffer_params_.height;
const int64_t pass_stride = effective_buffer_params_.pass_stride;
const int64_t row_stride = width * pass_stride;
const int64_t data_size = row_stride * height * sizeof(float);
const int64_t offset_y = effective_buffer_params_.full_y - effective_big_tile_params_.full_y;
const int64_t offset_in_floats = offset_y * row_stride;
const float *src = render_buffers->buffer.data() + offset_in_floats;
float *dst = buffers_->buffer.data();
memcpy(dst, src, data_size);
copy_render_buffers_to_device();
}
void PathTraceWork::copy_from_denoised_render_buffers(const RenderBuffers *render_buffers)
{
const int64_t width = effective_denoised_buffer_params_.width;
const int64_t offset_y = effective_denoised_buffer_params_.full_y -
effective_denoised_big_tile_params_.full_y;
const int64_t offset = offset_y * width;
render_buffers_host_copy_denoised(buffers_.get(),
effective_denoised_buffer_params_,
render_buffers,
effective_denoised_buffer_params_,
offset);
copy_render_buffers_to_device();
}
bool PathTraceWork::get_render_tile_pixels(const PassAccessor &pass_accessor,
const PassAccessor::Destination &destination)
{
const int offset_y = (effective_buffer_params_.full_y + effective_buffer_params_.window_y) -
(effective_big_tile_params_.full_y + effective_big_tile_params_.window_y);
const int width = effective_buffer_params_.width;
PassAccessor::Destination slice_destination = destination;
slice_destination.offset += offset_y * width;
return pass_accessor.get_render_tile_pixels(buffers_.get(), slice_destination);
}
bool PathTraceWork::set_render_tile_pixels(PassAccessor &pass_accessor,
const PassAccessor::Source &source)
{
const int offset_y = effective_buffer_params_.full_y - effective_big_tile_params_.full_y;
const int width = effective_buffer_params_.width;
PassAccessor::Source slice_source = source;
slice_source.offset += offset_y * width;
return pass_accessor.set_render_tile_pixels(buffers_.get(), slice_source);
}
PassAccessor::PassAccessInfo PathTraceWork::get_display_pass_access_info(PassMode pass_mode) const
{
const KernelFilm &kfilm = device_scene_->data.film;
const KernelBackground &kbackground = device_scene_->data.background;
const BufferParams &params = buffers_->params;
const BufferPass *display_pass = params.get_actual_display_pass(film_->get_display_pass());
if (display_pass == nullptr) {
/* Happens when interactive session changes display pass but render
* buffer does not contain it yet. */
return PassAccessor::PassAccessInfo();
}
PassAccessor::PassAccessInfo pass_access_info;
pass_access_info.type = display_pass->type;
pass_access_info.offset = PASS_UNUSED;
if (pass_mode == PassMode::DENOISED) {
pass_access_info.mode = PassMode::DENOISED;
pass_access_info.offset = params.get_pass_offset(pass_access_info.type, PassMode::DENOISED);
}
if (pass_access_info.offset == PASS_UNUSED) {
pass_access_info.mode = PassMode::NOISY;
pass_access_info.offset = params.get_pass_offset(pass_access_info.type);
}
pass_access_info.use_approximate_shadow_catcher = kfilm.use_approximate_shadow_catcher;
pass_access_info.use_approximate_shadow_catcher_background =
kfilm.use_approximate_shadow_catcher && !kbackground.transparent;
if (pass_access_info.mode == PassMode::DENOISED &&
(effective_denoised_buffer_params_.width != effective_buffer_params_.width ||
effective_denoised_buffer_params_.height != effective_buffer_params_.height))
{
/* Avoid using sample count to filter pass after upscaling, since it is stored at a different
* resolution. The denoiser should have applied scaling again in this case. */
pass_access_info.use_sample_count = false;
pass_access_info.use_approximate_shadow_catcher_background = false;
}
pass_access_info.show_active_pixels = film_->get_show_active_pixels();
return pass_access_info;
}
PassAccessor::Destination PathTraceWork::get_display_destination_template(
const PathTraceDisplay *display, const PassMode mode) const
{
PassAccessor::Destination destination(film_->get_display_pass(), mode);
const BufferParams &effective_big_tile_params = (mode == PassMode::DENOISED) ?
effective_denoised_big_tile_params_ :
effective_big_tile_params_;
const BufferParams &effective_buffer_params = (mode == PassMode::DENOISED) ?
effective_denoised_buffer_params_ :
effective_buffer_params_;
const int2 display_texture_size = display->get_texture_size();
const int texture_x = effective_buffer_params.full_x - effective_big_tile_params.full_x +
effective_buffer_params.window_x - effective_big_tile_params.window_x;
const int texture_y = effective_buffer_params.full_y - effective_big_tile_params.full_y +
effective_buffer_params.window_y - effective_big_tile_params.window_y;
destination.offset = texture_y * display_texture_size.x + texture_x;
destination.stride = display_texture_size.x;
return destination;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,202 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/pass_accessor.h"
#include "scene/pass.h"
#include "session/buffers.h"
#include "util/unique_ptr.h"
CCL_NAMESPACE_BEGIN
class BufferParams;
class Device;
class DeviceScene;
class Film;
class PathTraceDisplay;
class RenderBuffers;
class PathTraceWork {
public:
struct RenderStatistics {
float occupancy = 1.0f;
};
/* Create path trace work which fits best the device.
*
* The cancel request flag is used for a cheap check whether cancel is to be performed as soon as
* possible. This could be, for example, request to cancel rendering on camera navigation in
* viewport. */
static unique_ptr<PathTraceWork> create(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag);
virtual ~PathTraceWork();
/* Access the render buffers.
*
* Is only supposed to be used by the PathTrace to update buffer allocation and slicing to
* correspond to the big tile size and relative device performance. */
RenderBuffers *get_render_buffers();
/* Set effective parameters of the big tile and the work itself. */
void set_effective_buffer_params(const BufferParams &effective_big_tile_params,
const BufferParams &effective_buffer_params,
const BufferParams &effective_denoised_big_tile_params,
const BufferParams &effective_denoised_buffer_params);
/* Check whether the big tile is being worked on by multiple path trace works. */
bool has_multiple_works() const;
/* Allocate working memory for execution. Must be called before init_execution(). */
virtual void alloc_work_memory() {};
/* Initialize execution of kernels.
* Will ensure that all device queues are initialized for execution.
*
* This method is to be called after any change in the scene. It is not needed to call it prior
* to an every call of the `render_samples()`. */
virtual void init_execution() = 0;
/* Release resources acquired by init_execution(). */
virtual void deinit_execution() {}
/* Render given number of samples as a synchronous blocking call.
* The samples are added to the render buffer associated with this work. */
virtual void render_samples(RenderStatistics &statistics,
const int start_sample,
const int samples_num,
const int sample_offset) = 0;
/* Copy render result from this work to the corresponding place of the GPU display.
*
* The `pass_mode` indicates whether to access denoised or noisy version of the display pass. The
* noisy pass mode will be passed here when it is known that the buffer does not have denoised
* passes yet (because denoiser did not run). If the denoised pass is requested and denoiser is
* not used then this function will fall-back to the noisy pass instead. */
virtual void copy_to_display(PathTraceDisplay *display,
PassMode pass_mode,
const int num_samples) = 0;
virtual void destroy_gpu_resources(PathTraceDisplay *display) = 0;
/* Copy data from/to given render buffers.
* Will copy pixels from a corresponding place (from multi-device point of view) of the render
* buffers, and copy work's render buffers to the corresponding place of the destination. */
/* Notes:
* - Copies work's render buffer from the device.
* - Copies CPU-side buffer of the given buffer
* - Does not copy the buffer to its device. */
void copy_to_render_buffers(RenderBuffers *render_buffers);
/* Notes:
* - Does not copy given render buffers from the device.
* - Copies work's render buffer to its device. */
void copy_from_render_buffers(const RenderBuffers *render_buffers);
/* Special version of the `copy_from_render_buffers()` which only copies denoised passes from the
* given render buffers, leaving rest of the passes.
*
* Same notes about device copying applies to this call as well. */
void copy_from_denoised_render_buffers(const RenderBuffers *render_buffers);
/* Copy render buffers to/from device using an appropriate device queue when needed so that
* things are executed in order with the `render_samples()`. */
virtual bool copy_render_buffers_from_device() = 0;
virtual bool copy_render_buffers_to_device() = 0;
/* Zero render buffers to/from device using an appropriate device queue when needed so that
* things are executed in order with the `render_samples()`. */
virtual bool zero_render_buffers() = 0;
/* Access pixels rendered by this work and copy them to the corresponding location in the
* destination.
*
* NOTE: Does not perform copy of buffers from the device. Use `copy_render_tile_from_device()`
* to update host-side data. */
bool get_render_tile_pixels(const PassAccessor &pass_accessor,
const PassAccessor::Destination &destination);
/* Set pass data for baking. */
bool set_render_tile_pixels(PassAccessor &pass_accessor, const PassAccessor::Source &source);
/* Perform convergence test on the render buffer, and filter the convergence mask.
* Returns number of active pixels (the ones which did not converge yet). */
virtual int adaptive_sampling_converge_filter_count_active(const float threshold,
bool reset) = 0;
/* Denoise Volume Scattering Probability Guiding buffers. */
virtual void denoise_volume_guiding_buffers() = 0;
/* Run cryptomatte pass post-processing kernels. */
virtual void cryptomatte_postproces() = 0;
/* Cheap-ish request to see whether rendering is requested and is to be stopped as soon as
* possible, without waiting for any samples to be finished. */
bool is_cancel_requested() const
{
/* NOTE: Rely on the fact that on x86 CPU reading scalar can happen without atomic even in
* threaded environment. */
return *cancel_requested_flag_;
}
/* Access to the device which is used to path trace this work on. */
Device *get_device() const
{
return device_;
}
#if defined(WITH_PATH_GUIDING)
/* Initializes the per-thread guiding kernel data. */
virtual void guiding_init_kernel_globals(void * /*unused*/,
void * /*unused*/,
const bool /*unused*/)
{
}
#endif
protected:
PathTraceWork(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag);
PassAccessor::PassAccessInfo get_display_pass_access_info(PassMode pass_mode) const;
/* Get destination which offset and stride are configured so that writing to it will write to a
* proper location of GPU display texture, taking current tile and device slice into account. */
PassAccessor::Destination get_display_destination_template(const PathTraceDisplay *display,
const PassMode mode) const;
/* Device which will be used for path tracing.
* Note that it is an actual render device (and never is a multi-device). */
Device *device_;
/* Film is used to access display pass configuration for GPU display update.
* Note that only fields which are not a part of kernel data can be accessed via the Film. */
Film *film_;
/* Device side scene storage, that may be used for integrator logic. */
DeviceScene *device_scene_;
/* Render buffers where sampling is being accumulated into, allocated for a fraction of the big
* tile which is being rendered by this work.
* It also defines possible subset of a big tile in the case of multi-device rendering. */
unique_ptr<RenderBuffers> buffers_;
/* Effective parameters of the big tile, and current work render buffer.
* The latter might be different from `buffers_->params` when there is a resolution divider
* involved. */
BufferParams effective_big_tile_params_;
BufferParams effective_buffer_params_;
BufferParams effective_denoised_big_tile_params_;
BufferParams effective_denoised_buffer_params_;
const bool *cancel_requested_flag_ = nullptr;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,467 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/path_trace_work_cpu.h"
#include "device/cpu/kernel.h"
#include "device/device.h"
#ifdef WITH_CYCLES_DEBUG
# include "kernel/film/write.h"
#endif
#include "kernel/integrator/path_state.h"
#include "integrator/pass_accessor_cpu.h"
#include "integrator/path_trace_display.h"
#include "scene/scene.h"
#include "session/buffers.h"
#include "util/tbb.h"
#include "util/time.h"
CCL_NAMESPACE_BEGIN
/* Create TBB arena for execution of path tracing and rendering tasks. */
static inline tbb::task_arena local_tbb_arena_create(const Device *device)
{
/* TODO: limit this to number of threads of CPU device, it may be smaller than
* the system number of threads when we reduce the number of CPU threads in
* CPU + GPU rendering to dedicate some cores to handling the GPU device. */
return tbb::task_arena(device->info.cpu_threads);
}
/* Get ThreadKernelGlobalsCPU for the current thread. */
static inline ThreadKernelGlobalsCPU *kernel_thread_globals_get(
vector<ThreadKernelGlobalsCPU> &kernel_thread_globals)
{
const int thread_index = tbb::this_task_arena::current_thread_index();
DCHECK_GE(thread_index, 0);
DCHECK_LE(thread_index, kernel_thread_globals.size());
return &kernel_thread_globals[thread_index];
}
PathTraceWorkCPU::PathTraceWorkCPU(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag)
: PathTraceWork(device, film, device_scene, cancel_requested_flag),
kernels_(Device::get_cpu_kernels())
{
DCHECK_EQ(device->info.type, DEVICE_CPU);
}
void PathTraceWorkCPU::init_execution()
{
/* Acquire thread globals, updating all data pointers. */
kernel_thread_globals_ = device_->acquire_cpu_kernel_thread_globals();
}
void PathTraceWorkCPU::deinit_execution()
{
device_->release_cpu_kernel_thread_globals();
kernel_thread_globals_ = nullptr;
}
void PathTraceWorkCPU::render_samples(RenderStatistics &statistics,
const int start_sample,
const int samples_num,
const int sample_offset)
{
const int64_t image_width = effective_buffer_params_.width;
const int64_t image_height = effective_buffer_params_.height;
const int64_t total_pixels_num = image_width * image_height;
if (device_->profiler.active()) {
for (ThreadKernelGlobalsCPU &kernel_globals : *kernel_thread_globals_) {
kernel_globals.start_profiling();
}
}
tbb::task_arena local_arena = local_tbb_arena_create(device_);
local_arena.execute([&]() {
parallel_for(int64_t(0), total_pixels_num, [&](int64_t work_index) {
if (is_cancel_requested()) {
return;
}
const int y = work_index / image_width;
const int x = work_index - y * image_width;
KernelWorkTile work_tile;
work_tile.x = effective_buffer_params_.full_x + x;
work_tile.y = effective_buffer_params_.full_y + y;
work_tile.w = 1;
work_tile.h = 1;
work_tile.start_sample = start_sample;
work_tile.sample_offset = sample_offset;
work_tile.num_samples = 1;
work_tile.offset = effective_buffer_params_.offset;
work_tile.stride = effective_buffer_params_.stride;
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_get(*kernel_thread_globals_);
render_samples_full_pipeline(kernel_globals, work_tile, samples_num);
});
});
if (device_->profiler.active()) {
for (ThreadKernelGlobalsCPU &kernel_globals : *kernel_thread_globals_) {
kernel_globals.stop_profiling();
}
}
statistics.occupancy = 1.0f;
}
void PathTraceWorkCPU::render_samples_full_pipeline(ThreadKernelGlobalsCPU *kernel_globals,
const KernelWorkTile &work_tile,
const int samples_num)
{
const bool has_bake = device_scene_->data.bake.use;
IntegratorStateCPU integrator_states[2];
IntegratorStateCPU *state = &integrator_states[0];
IntegratorStateCPU *shadow_catcher_state = nullptr;
if (device_scene_->data.integrator.has_shadow_catcher) {
shadow_catcher_state = &integrator_states[1];
path_state_init_queues(shadow_catcher_state);
}
KernelWorkTile sample_work_tile = work_tile;
float *render_buffer = buffers_->buffer.data();
fast_timer render_timer;
for (int sample = 0; sample < samples_num; ++sample) {
if (is_cancel_requested()) {
break;
}
if (has_bake) {
if (!kernels_.integrator_init_from_bake(
kernel_globals, state, &sample_work_tile, render_buffer))
{
break;
}
}
else {
if (!kernels_.integrator_init_from_camera(
kernel_globals, state, &sample_work_tile, render_buffer))
{
break;
}
}
#if defined(WITH_PATH_GUIDING)
if (kernel_globals->data.integrator.train_guiding) {
assert(kernel_globals->opgl_path_segment_storage);
assert(kernel_globals->opgl_path_segment_storage->GetNumSegments() == 0);
kernels_.integrator_megakernel(kernel_globals, state, render_buffer);
/* Push the generated sample data to the global sample data storage. */
guiding_push_sample_data_to_global_storage(kernel_globals, state, render_buffer);
/* No training for shadow catcher paths. */
if (shadow_catcher_state) {
kernel_globals->data.integrator.train_guiding = false;
kernels_.integrator_megakernel(kernel_globals, shadow_catcher_state, render_buffer);
kernel_globals->data.integrator.train_guiding = true;
}
}
else
#endif
{
kernels_.integrator_megakernel(kernel_globals, state, render_buffer);
if (shadow_catcher_state) {
kernels_.integrator_megakernel(kernel_globals, shadow_catcher_state, render_buffer);
}
}
if (kernel_globals->data.film.pass_render_time != PASS_UNUSED) {
uint64_t time;
if (render_timer.lap(time)) {
ccl_global float *buffer = render_buffer + (uint64_t)state->path.render_pixel_index *
kernel_globals->data.film.pass_stride;
*(buffer + kernel_globals->data.film.pass_render_time) += float(time);
}
}
++sample_work_tile.start_sample;
}
}
void PathTraceWorkCPU::copy_to_display(PathTraceDisplay *display,
PassMode pass_mode,
const int num_samples)
{
half4 *rgba_half = display->map_texture_buffer();
if (!rgba_half) {
/* TODO(sergey): Look into using copy_to_display() if mapping failed. Might be needed for
* some implementations of PathTraceDisplay which can not map memory? */
return;
}
const KernelFilm &kfilm = device_scene_->data.film;
const PassAccessor::PassAccessInfo pass_access_info = get_display_pass_access_info(pass_mode);
if (pass_access_info.type == PASS_NONE) {
return;
}
const BufferParams &effective_buffer_params = (pass_mode == PassMode::DENOISED) ?
effective_denoised_buffer_params_ :
effective_buffer_params_;
const PassAccessorCPU pass_accessor(pass_access_info, kfilm.exposure, num_samples);
PassAccessor::Destination destination = get_display_destination_template(display, pass_mode);
destination.pixels_half_rgba = rgba_half;
tbb::task_arena local_arena = local_tbb_arena_create(device_);
local_arena.execute([&]() {
pass_accessor.get_render_tile_pixels(buffers_.get(), effective_buffer_params, destination);
});
display->unmap_texture_buffer();
}
void PathTraceWorkCPU::destroy_gpu_resources(PathTraceDisplay * /*display*/) {}
bool PathTraceWorkCPU::copy_render_buffers_from_device()
{
return buffers_->copy_from_device();
}
bool PathTraceWorkCPU::copy_render_buffers_to_device()
{
buffers_->buffer.copy_to_device();
return true;
}
bool PathTraceWorkCPU::zero_render_buffers()
{
buffers_->zero();
return true;
}
int PathTraceWorkCPU::adaptive_sampling_converge_filter_count_active(const float threshold,
bool reset)
{
const int full_x = effective_buffer_params_.full_x;
const int full_y = effective_buffer_params_.full_y;
const int width = effective_buffer_params_.width;
const int height = effective_buffer_params_.height;
const int offset = effective_buffer_params_.offset;
const int stride = effective_buffer_params_.stride;
float *render_buffer = buffers_->buffer.data();
uint num_active_pixels = 0;
tbb::task_arena local_arena = local_tbb_arena_create(device_);
/* Check convergency and do x-filter in a single `parallel_for`, to reduce threading overhead. */
local_arena.execute([&]() {
parallel_for(full_y, full_y + height, [&](int y) {
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_->data();
bool row_converged = true;
uint num_row_pixels_active = 0;
for (int x = 0; x < width; ++x) {
if (!kernels_.adaptive_sampling_convergence_check(
kernel_globals, render_buffer, full_x + x, y, threshold, reset, offset, stride))
{
++num_row_pixels_active;
row_converged = false;
}
}
atomic_fetch_and_add_uint32(&num_active_pixels, num_row_pixels_active);
if (!row_converged) {
kernels_.adaptive_sampling_filter_x(
kernel_globals, render_buffer, y, full_x, width, offset, stride);
}
});
});
if (num_active_pixels) {
local_arena.execute([&]() {
parallel_for(full_x, full_x + width, [&](int x) {
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_->data();
kernels_.adaptive_sampling_filter_y(
kernel_globals, render_buffer, x, full_y, height, offset, stride);
});
});
}
return num_active_pixels;
}
void PathTraceWorkCPU::cryptomatte_postproces()
{
const int width = effective_buffer_params_.width;
const int height = effective_buffer_params_.height;
float *render_buffer = buffers_->buffer.data();
tbb::task_arena local_arena = local_tbb_arena_create(device_);
/* Check convergency and do x-filter in a single `parallel_for`, to reduce threading overhead. */
local_arena.execute([&]() {
parallel_for(0, height, [&](int y) {
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_->data();
int pixel_index = y * width;
for (int x = 0; x < width; ++x, ++pixel_index) {
kernels_.cryptomatte_postprocess(kernel_globals, render_buffer, pixel_index);
}
});
});
}
void PathTraceWorkCPU::denoise_volume_guiding_buffers()
{
const int min_x = effective_buffer_params_.full_x;
const int min_y = effective_buffer_params_.full_y;
const int max_x = effective_buffer_params_.width + min_x;
const int max_y = effective_buffer_params_.height + min_y;
const int offset = effective_buffer_params_.offset;
const int stride = effective_buffer_params_.stride;
float *render_buffer = buffers_->buffer.data();
tbb::task_arena local_arena = local_tbb_arena_create(device_);
const blocked_range2d<int> range(min_x, max_x, min_y, max_y);
/* Filter in x direction. */
local_arena.execute([&]() {
parallel_for(range, [&](const blocked_range2d<int> r) {
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_->data();
for (int y = r.cols().begin(); y < r.cols().end(); ++y) {
for (int x = r.rows().begin(); x < r.rows().end(); ++x) {
kernels_.volume_guiding_filter_x(
kernel_globals, render_buffer, y, x, min_x, max_x, offset, stride);
}
}
});
});
/* Filter in y direction. Unlike `filter_x`, the inner loop of `filter_y` is serially run inside
* the kernel, to avoid the need of intermediate buffers. */
local_arena.execute([&]() {
parallel_for(min_x, max_x, [&](int x) {
ThreadKernelGlobalsCPU *kernel_globals = kernel_thread_globals_->data();
kernels_.volume_guiding_filter_y(
kernel_globals, render_buffer, x, min_y, max_y, offset, stride);
});
});
}
#if defined(WITH_PATH_GUIDING)
/* NOTE: It seems that this is called before every rendering iteration/progression and not once per
* rendering. May be we find a way to call it only once per rendering. */
void PathTraceWorkCPU::guiding_init_kernel_globals(void *guiding_field,
void *sample_data_storage,
const bool train)
{
/* Linking the global guiding structures (e.g., Field and SampleStorage) to the per-thread
* kernel globals. */
for (int thread_index = 0; thread_index < kernel_thread_globals_->size(); thread_index++) {
ThreadKernelGlobalsCPU &kg = (*kernel_thread_globals_)[thread_index];
openpgl::cpp::Field *field = (openpgl::cpp::Field *)guiding_field;
/* Allocate sampling distributions. */
kg.opgl_guiding_field = field;
# if PATH_GUIDING_LEVEL >= 4
if (kg.opgl_surface_sampling_distribution) {
kg.opgl_surface_sampling_distribution.reset();
}
if (kg.opgl_volume_sampling_distribution) {
kg.opgl_volume_sampling_distribution.reset();
}
if (field) {
kg.opgl_surface_sampling_distribution =
make_unique<openpgl::cpp::SurfaceSamplingDistribution>(field);
kg.opgl_volume_sampling_distribution = make_unique<openpgl::cpp::VolumeSamplingDistribution>(
field);
}
# endif
/* Reserve storage for training. */
kg.data.integrator.train_guiding = train;
kg.opgl_sample_data_storage = (openpgl::cpp::SampleStorage *)sample_data_storage;
if (train) {
kg.opgl_path_segment_storage->Reserve(kg.data.integrator.transparent_max_bounce +
kg.data.integrator.max_bounce + 3);
kg.opgl_path_segment_storage->Clear();
}
}
}
void PathTraceWorkCPU::guiding_push_sample_data_to_global_storage(ThreadKernelGlobalsCPU *kg,
IntegratorStateCPU *state,
ccl_global float *ccl_restrict
render_buffer)
{
# ifdef WITH_CYCLES_DEBUG
if (LOG_IS_ON(LOG_LEVEL_DEBUG)) {
/* Check if the generated path segments contain valid values. */
const bool validSegments = kg->opgl_path_segment_storage->ValidateSegments();
if (!validSegments) {
LOG_DEBUG << "Guiding: invalid path segments!";
}
}
/* Write debug render pass to validate it matches combined pass. */
pgl_vec3f pgl_final_color = kg->opgl_path_segment_storage->CalculatePixelEstimate(false);
ccl_global float *buffer = film_pass_pixel_render_buffer(kg, state, render_buffer);
float3 final_color = make_float3(pgl_final_color.x, pgl_final_color.y, pgl_final_color.z);
if (kernel_data.film.pass_guiding_color != PASS_UNUSED) {
film_write_pass_float3(buffer + kernel_data.film.pass_guiding_color, final_color);
}
# else
(void)state;
(void)render_buffer;
# endif
/* Convert the path segment representation of the random walk into radiance samples. */
# if PATH_GUIDING_LEVEL >= 2
const bool use_direct_light = kernel_data.integrator.use_guiding_direct_light;
const bool use_mis_weights = kernel_data.integrator.use_guiding_mis_weights;
kg->opgl_path_segment_storage->PrepareSamples(use_mis_weights, use_direct_light, false);
# endif
# ifdef WITH_CYCLES_DEBUG
/* Check if the training/radiance samples generated by the path segment storage are valid. */
if (LOG_IS_ON(LOG_LEVEL_DEBUG)) {
const bool validSamples = kg->opgl_path_segment_storage->ValidateSamples();
if (!validSamples) {
LOG_DEBUG
<< "Guiding: path segment storage generated/contains invalid radiance/training samples!";
}
}
# endif
# if PATH_GUIDING_LEVEL >= 3
/* Push radiance samples from current random walk/path to the global sample storage. */
size_t num_samples = 0;
const openpgl::cpp::SampleData *samples = kg->opgl_path_segment_storage->GetSamples(num_samples);
kg->opgl_sample_data_storage->AddSamples(samples, num_samples);
# endif
/* Clear storage for the current path, to be ready for the next path. */
kg->opgl_path_segment_storage->Clear();
}
#endif
CCL_NAMESPACE_END

View File

@@ -0,0 +1,87 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/device/cpu/globals.h"
#include "kernel/integrator/state.h"
#include "device/queue.h"
#include "integrator/path_trace_work.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
struct KernelWorkTile;
struct ThreadKernelGlobalsCPU;
struct IntegratorStateCPU;
class CPUKernels;
/* Implementation of PathTraceWork which schedules work on to queues pixel-by-pixel,
* for CPU devices.
*
* NOTE: For the CPU rendering there are assumptions about TBB arena size and number of concurrent
* queues on the render device which makes this work be only usable on CPU. */
class PathTraceWorkCPU : public PathTraceWork {
public:
PathTraceWorkCPU(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag);
void init_execution() override;
void deinit_execution() override;
void render_samples(RenderStatistics &statistics,
const int start_sample,
const int samples_num,
const int sample_offset) override;
void copy_to_display(PathTraceDisplay *display,
PassMode pass_mode,
const int num_samples) override;
void destroy_gpu_resources(PathTraceDisplay *display) override;
bool copy_render_buffers_from_device() override;
bool copy_render_buffers_to_device() override;
bool zero_render_buffers() override;
int adaptive_sampling_converge_filter_count_active(const float threshold, bool reset) override;
void cryptomatte_postproces() override;
void denoise_volume_guiding_buffers() override;
#if defined(WITH_PATH_GUIDING)
/* Initializes the per-thread guiding kernel data. The function sets the pointers to the
* global guiding field and the sample data storage as well es initializes the per-thread
* guided sampling distributions (e.g., SurfaceSamplingDistribution and
* VolumeSamplingDistribution). */
void guiding_init_kernel_globals(void *guiding_field,
void *sample_data_storage,
const bool train) override;
/* Pushes the collected training data/samples of a path to the global sample storage.
* This function is called at the end of a random walk/path generation. */
void guiding_push_sample_data_to_global_storage(ThreadKernelGlobalsCPU *kg,
IntegratorStateCPU *state,
ccl_global float *ccl_restrict render_buffer);
#endif
protected:
/* Core path tracing routine. Renders given work time on the given queue. */
void render_samples_full_pipeline(ThreadKernelGlobalsCPU *kernel_globals,
const KernelWorkTile &work_tile,
const int samples_num);
/* CPU kernels. */
const CPUKernels &kernels_;
/* Pointer to device-owned kernel globals which is suitable for concurrent access from multiple
* threads. This allows dynamic updates to image_info when textures are loaded on demand. */
vector<ThreadKernelGlobalsCPU> *kernel_thread_globals_ = nullptr;
};
CCL_NAMESPACE_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "kernel/integrator/state.h"
#include "device/graphics_interop.h"
#include "device/memory.h"
#include "device/queue.h"
#include "integrator/path_trace_work.h"
#include "integrator/work_tile_scheduler.h"
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
struct KernelWorkTile;
/* Implementation of PathTraceWork which schedules work to the device in tiles which are sized
* to match device queue's number of path states.
* This implementation suits best devices which have a lot of integrator states, such as GPU. */
class PathTraceWorkGPU : public PathTraceWork {
public:
PathTraceWorkGPU(Device *device,
Film *film,
DeviceScene *device_scene,
const bool *cancel_requested_flag);
void alloc_work_memory() override;
void init_execution() override;
void render_samples(RenderStatistics &statistics,
const int start_sample,
const int samples_num,
const int sample_offset) override;
void copy_to_display(PathTraceDisplay *display,
PassMode pass_mode,
const int num_samples) override;
void destroy_gpu_resources(PathTraceDisplay *display) override;
bool copy_render_buffers_from_device() override;
bool copy_render_buffers_to_device() override;
bool zero_render_buffers() override;
int adaptive_sampling_converge_filter_count_active(const float threshold, bool reset) override;
void cryptomatte_postproces() override;
void denoise_volume_guiding_buffers() override;
protected:
void alloc_integrator_soa();
void alloc_integrator_queue();
void alloc_integrator_sorting();
void alloc_integrator_path_split();
/* Returns DEVICE_KERNEL_NUM if there are no scheduled kernels. */
DeviceKernel get_most_queued_kernel() const;
void enqueue_reset();
bool enqueue_work_tiles(bool &finished);
void enqueue_work_tiles(DeviceKernel kernel,
const KernelWorkTile work_tiles[],
const int num_work_tiles,
const int num_active_paths,
const int num_predicted_splits);
bool enqueue_path_iteration();
void enqueue_path_iteration(DeviceKernel kernel, const int num_paths_limit = INT_MAX);
bool update_queue_counter_and_cache();
void compute_queued_paths(DeviceKernel kernel, DeviceKernel queued_kernel);
void compute_sorted_queued_paths(DeviceKernel queued_kernel, const int num_paths_limit);
void compact_main_paths(const int num_active_paths);
void compact_shadow_paths();
void compact_paths(const int num_active_paths,
const int max_active_path_index,
DeviceKernel terminated_paths_kernel,
DeviceKernel compact_paths_kernel,
DeviceKernel compact_kernel);
int num_active_main_paths_paths();
/* Check whether graphics interop can be used for the PathTraceDisplay update. */
bool should_use_graphics_interop(PathTraceDisplay *display);
/* Naive implementation of the `copy_to_display()` which performs film conversion on the
* device, then copies pixels to the host and pushes them to the `display`. */
void copy_to_display_naive(PathTraceDisplay *display, PassMode pass_mode, const int num_samples);
/* Implementation of `copy_to_display()` which uses driver's OpenGL/GPU interoperability
* functionality, avoiding copy of pixels to the host. */
bool copy_to_display_interop(PathTraceDisplay *display,
PassMode pass_mode,
const int num_samples);
/* Synchronously run film conversion kernel and store display result in the given destination. */
void get_render_tile_film_pixels(const PassAccessor::Destination &destination,
PassMode pass_mode,
int num_samples);
int adaptive_sampling_convergence_check_count_active(const float threshold, bool reset);
void enqueue_adaptive_sampling_filter_x();
void enqueue_adaptive_sampling_filter_y();
bool has_shadow_catcher() const;
/* Count how many currently scheduled paths can still split. */
int shadow_catcher_count_possible_splits();
/* Kernel properties. */
bool kernel_uses_sorting(DeviceKernel kernel);
bool kernel_creates_shadow_paths(DeviceKernel kernel);
bool kernel_creates_ao_paths(DeviceKernel kernel);
bool kernel_is_shadow_path(DeviceKernel kernel);
int kernel_max_active_main_path_index(DeviceKernel kernel);
/* Integrator queue. */
unique_ptr<DeviceQueue> queue_;
/* Scheduler which gives work to path tracing threads. */
WorkTileScheduler work_tile_scheduler_;
/* Integrate state for paths. */
IntegratorStateGPU integrator_state_gpu_;
/* SoA arrays for integrator state. */
vector<unique_ptr<device_memory>> integrator_state_soa_;
uint integrator_state_soa_kernel_features_;
int integrator_state_soa_volume_stack_size_ = 0;
/* Keep track of number of queued kernels. */
device_vector<IntegratorQueueCounter> integrator_queue_counter_;
/* Shader sorting. */
device_vector<int> integrator_shader_sort_counter_;
device_vector<int> integrator_shader_raytrace_sort_counter_;
device_vector<int> integrator_shader_sort_prefix_sum_;
device_vector<int> integrator_shader_sort_partition_key_offsets_;
/* Path split. */
device_vector<int> integrator_next_main_path_index_;
device_vector<int> integrator_next_shadow_path_index_;
/* Temporary buffer to get an array of queued path for a particular kernel. */
device_vector<int> queued_paths_;
device_vector<int> num_queued_paths_;
/* Temporary buffer for passing work tiles to kernel. */
device_vector<KernelWorkTile> work_tiles_;
/* Temporary buffer used by the copy_to_display() whenever graphics interoperability is not
* available. Is allocated on-demand. */
device_vector<half4> display_rgba_half_;
unique_ptr<DeviceGraphicsInterop> device_graphics_interop_;
/* Cached result of device->should_use_graphics_interop(). */
bool interop_use_checked_ = false;
bool interop_use_ = false;
/* Number of partitions to sort state indices into prior to material sort. */
int num_sort_partitions_;
/* Maximum number of concurrent integrator states. */
int max_num_paths_;
/* Minimum number of paths which keeps the device bust. If the actual number of paths falls below
* this value more work will be scheduled. */
int min_num_active_main_paths_;
/* Maximum path index, effective number of paths used may be smaller than
* the size of the integrator_state_ buffer so can avoid iterating over the
* full buffer. */
int max_active_main_path_index_;
};
CCL_NAMESPACE_END

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,511 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/adaptive_sampling.h"
#include "integrator/denoiser.h"
#include "session/buffers.h"
#include "util/string.h"
CCL_NAMESPACE_BEGIN
class SessionParams;
class TileManager;
class RenderWork {
public:
float resolution_divider = 1;
float denoised_resolution_divider = 1;
/* Initialize render buffers.
* Includes steps like zeroing the buffer on the device, and optional reading of pixels from the
* baking target. */
bool init_render_buffers = false;
/* Path tracing samples information. */
struct {
int start_sample = 0;
int num_samples = 0;
int sample_offset = 0;
} path_trace;
struct {
/* Check for convergency and filter the mask. */
bool filter = false;
float threshold = 0.0f;
/* Reset convergency flag when filtering, forcing a re-check of whether pixel did converge. */
bool reset = false;
} adaptive_sampling;
struct {
bool postprocess = false;
} cryptomatte;
/* Work related on the current tile. */
struct {
/* Write render buffers of the current tile.
*
* It is up to the path trace to decide whether writing should happen via user-provided
* callback into the rendering software, or via tile manager into a partial file. */
bool write = false;
bool denoise = false;
} tile;
/* Work related on the full-frame render buffer. */
struct {
/* Write full render result.
* Implies reading the partial file from disk. */
bool write = false;
} full;
/* Display which is used to visualize render result. */
struct {
/* Display needs to be updated for the new render. */
bool update = false;
/* Display can use denoised result if available. */
bool use_denoised_result = true;
} display;
/* Re-balance multi-device scheduling after rendering this work.
* Note that the scheduler does not know anything about devices, so if there is only a single
* device used, then it is up for the PathTracer to ignore the balancing. */
bool rebalance = false;
/* Perform volume guiding buffer denoise. */
bool volume_guiding_denoise = false;
/* Conversion to bool, to simplify checks about whether there is anything to be done for this
* work. */
operator bool() const
{
return path_trace.num_samples || adaptive_sampling.filter || display.update || tile.denoise ||
tile.write || full.write;
}
};
class RenderScheduler {
public:
RenderScheduler(TileManager &tile_manager, const SessionParams &params);
/* Specify whether cryptomatte-related works are to be scheduled. */
void set_need_schedule_cryptomatte(bool need_schedule_cryptomatte);
/* Allows to disable work re-balancing works, allowing to schedule as much to a single device
* as possible. */
void set_need_schedule_rebalance(bool need_schedule_rebalance);
bool is_background() const;
void set_denoiser_params(const DenoiseParams &params);
bool is_denoiser_gpu_used() const;
void set_adaptive_sampling(const AdaptiveSampling &adaptive_sampling);
bool is_adaptive_sampling_used() const;
/* Setup parameters defining the sampling range.
*
* It is a single function setting up multiple parameters because there are inter-dependencies
* between these parameters.
*
* In simple cases the subset is not used and the given num_samples samples is rendered, and the
* subset length and offset are ignored.
*
* It is possible to render a subset of the overall samples. This is typically used to distribute
* rendering of a single frame across multiple computers. This subset rendering is enabled by
* setting use_sample_subset=true, and giving the desired offset and length of the subset. The
* subset offset is a 0-based sample index to start sampling from, and the length is the number
* of samples to render in this subset.
*
* When the subset rendering is enabled, num_samples is expected to be set to the overall number
* of samples to be rendered, and it is internally used to clamp the number of samples rendered
* by a subset. */
void set_sample_params(const int num_samples,
const bool use_sample_subset,
const int sample_subset_offset,
const int sample_subset_length);
/* Number of samples to render, starting from start sample.
* The scheduler will schedule work in the range of
* [start_sample, start_sample + num_samples - 1], inclusively. */
int get_num_samples() const;
/* For sample subset rendering, extra offset to be added to sample index
* for the sampling pattern to be shifted. */
int get_sample_offset() const;
/* Time limit for the path tracing tasks, in minutes.
* Zero disables the limit. */
void set_time_limit(const double time_limit);
double get_time_limit() const;
/* Get sample up to which rendering has been done.
* This is an absolute 0-based value.
*
* For example, if start sample is 10 and 5 samples were rendered, then this call will
* return 14.
*
* If there were no samples rendered, then the behavior is undefined. */
int get_rendered_sample() const;
/* Get number of samples rendered within the current scheduling session.
*
* For example, if start sample is 10 and 5 samples were rendered, then this call will
* return 5.
*
* Note that this is based on the scheduling information. In practice this means that if someone
* requested for work to render the scheduler considers the work done. */
int get_num_rendered_samples() const;
/* Reset scheduler, indicating that rendering will happen from scratch.
* Resets current rendered state, as well as scheduling information. */
void reset(const BufferParams &buffer_params);
/* Reset scheduler upon switching to a next tile.
* Will keep the same number of samples and full-frame render parameters, but will reset progress
* and allow schedule renders works from the beginning of the new tile. */
void reset_for_next_tile();
/* Reschedule adaptive sampling work when all pixels did converge.
* If there is nothing else to be done for the adaptive sampling (pixels did converge to the
* final threshold) then false is returned and the render scheduler will stop scheduling path
* tracing works. Otherwise will modify the work's adaptive sampling settings to continue with
* a lower threshold. */
bool render_work_reschedule_on_converge(RenderWork &render_work);
/* Reschedule adaptive sampling work when the device is mostly on idle, but not all pixels yet
* converged.
* If re-scheduling is not possible (adaptive sampling is happening with the final threshold, and
* the path tracer is to finish the current pixels) then false is returned. */
bool render_work_reschedule_on_idle(RenderWork &render_work);
/* Reschedule work when rendering has been requested to cancel.
*
* Will skip all work which is not needed anymore because no more samples will be added (for
* example, adaptive sampling filtering and convergence check will be skipped).
* Will enable all work needed to make sure all passes are communicated to the software.
*
* NOTE: Should be used before passing work to `PathTrace::render_samples()`. */
void render_work_reschedule_on_cancel(RenderWork &render_work);
RenderWork get_render_work();
/* Report that the path tracer started to work, after scene update and loading kernels. */
void report_work_begin(const RenderWork &render_work);
/* Report time (in seconds) which corresponding part of work took. */
void report_path_trace_time(const RenderWork &render_work, const double time, bool is_cancelled);
void report_path_trace_occupancy(const RenderWork &render_work, const float occupancy);
void report_adaptive_filter_time(const RenderWork &render_work,
const double time,
bool is_cancelled);
void report_denoise_time(const RenderWork &render_work, const double time);
void report_display_update_time(const RenderWork &render_work, const double time);
void report_rebalance_time(const RenderWork &render_work,
const double time,
bool balance_changed);
void report_volume_guiding_denoise_time(const RenderWork &render_work, const double time);
/* Generate full multi-line report of the rendering process, including rendering parameters,
* times, and so on. */
string full_report() const;
void set_limit_samples_per_update(const int limit_samples);
protected:
/* Check whether all work has been scheduled and time limit was not exceeded.
*
* NOTE: Tricky bit: if the time limit was reached the done() is considered to be true, but some
* extra work needs to be scheduled to denoise and write final result. */
bool done() const;
/* Update scheduling state for a newly scheduled work.
* Takes care of things like checking whether work was ever denoised, tile was written and states
* like that. */
void update_state_for_render_work(const RenderWork &render_work);
/* Returns true if any work was scheduled. */
bool set_postprocess_render_work(RenderWork *render_work);
/* Set work which is to be performed after all tiles has been rendered. */
void set_full_frame_render_work(RenderWork *render_work);
/* Update start resolution divider based on the accumulated timing information, preserving nice
* feeling navigation feel. */
void update_start_resolution_divider();
/* Calculate desired update interval in seconds based on the current timings and settings.
* Will give an interval which provides good feeling updates during viewport navigation. */
double guess_viewport_navigation_update_interval_in_seconds() const;
/* Check whether denoising is active during interactive update while resolution divider is not
* unit. */
bool is_denoise_active_during_update() const;
/* Heuristic which aims to give perceptually pleasant update of display interval in a way that at
* lower samples and near the beginning of rendering, updates happen more often, but with higher
* number of samples and later in the render, updates happen less often but device occupancy
* goes higher. */
double guess_display_update_interval_in_seconds() const;
double guess_display_update_interval_in_seconds_for_num_samples(
const int num_rendered_samples) const;
double guess_display_update_interval_in_seconds_for_num_samples_no_limit(
int num_rendered_samples) const;
/* Calculate number of samples which can be rendered within current desired update interval which
* is calculated by `guess_update_interval_in_seconds()`. */
int calculate_num_samples_per_update() const;
/* Get start sample and the number of samples which are to be path traces in the current work. */
int get_start_sample_to_path_trace() const;
int get_num_samples_to_path_trace() const;
/* Calculate how many samples there are to be rendered for the very first path trace after reset.
*/
int get_num_samples_during_navigation(const int resolution_divider) const;
/* Whether adaptive sampling convergence check and filter is to happen. */
bool work_need_adaptive_filter() const;
/* Calculate threshold for adaptive sampling. */
float work_adaptive_threshold() const;
/* Check whether current work needs denoising.
* Denoising is not needed if the denoiser is not configured, or when denoising is happening too
* often.
*
* The delayed will be true when the denoiser is configured for use, but it was delayed for a
* later sample, to reduce overhead.
*
* ready_to_display will be false if we may have a denoised result that is outdated due to
* increased samples. */
bool work_need_denoise(bool &delayed, bool &ready_to_display);
/* Check whether current work need to update display.
*
* The `denoiser_delayed` is what `work_need_denoise()` returned as delayed denoiser flag. */
bool work_need_update_display(const bool denoiser_delayed);
/* Check whether it is time to perform rebalancing for the render work, */
bool work_need_rebalance();
/* Check whether timing of the given work are usable to store timings in the `first_render_time_`
* for the resolution divider calculation. */
bool work_is_usable_for_first_render_estimation(const RenderWork &render_work);
/* Check whether timing report about the given work need to reset accumulated average time. */
bool work_report_reset_average(const RenderWork &render_work);
/* Check whether render time limit has been reached (or exceeded), and if so store related
* information in the state so that rendering is considered finished, and is possible to report
* average render time information. */
void check_time_limit_reached();
/* Helper class to keep track of task timing.
*
* Contains two parts: wall time and average. The wall time is an actual wall time of how long it
* took to complete all tasks of a type. Is always advanced when PathTracer reports time update.
*
* The average time is used for scheduling purposes. It is estimated to be a time of how long it
* takes to perform task on the final resolution. */
class TimeWithAverage {
public:
void reset()
{
total_wall_time_ = 0.0;
average_time_accumulator_ = 0.0;
num_average_times_ = 0;
last_sample_time_ = 0.0;
}
void add_wall(const double time)
{
total_wall_time_ += time;
}
void add_average(const double time, const int num_measurements = 1)
{
average_time_accumulator_ += time;
num_average_times_ += num_measurements;
last_sample_time_ = time / num_measurements;
}
double get_wall() const
{
return total_wall_time_;
}
double get_average() const
{
if (num_average_times_ == 0) {
return 0;
}
return average_time_accumulator_ / num_average_times_;
}
double get_last_sample_time() const
{
return last_sample_time_;
}
void reset_average()
{
average_time_accumulator_ = 0.0;
num_average_times_ = 0;
}
protected:
double total_wall_time_ = 0.0;
double average_time_accumulator_ = 0.0;
int num_average_times_ = 0;
double last_sample_time_ = 0.0;
};
struct {
bool user_is_navigating = false;
int resolution_divider = 1;
/* Number of rendered samples on top of the start sample. */
int num_rendered_samples = 0;
/* Point in time the latest PathTraceDisplay work has been scheduled. */
double last_display_update_time = 0.0;
/* Value of -1 means display was never updated. */
int last_display_update_sample = -1;
/* Point in time at which last rebalance has been performed. */
double last_rebalance_time = 0.0;
/* Number of rebalance works which has been requested to be performed.
* The path tracer might ignore the work if there is a single device rendering. */
int num_rebalance_requested = 0;
/* Number of rebalance works handled which did change balance across devices. */
int num_rebalance_changes = 0;
bool need_rebalance_at_next_work = false;
/* Denotes whether the latest performed rebalance work cause an actual rebalance of work across
* devices. */
bool last_rebalance_changed = false;
/* Threshold for adaptive sampling which will be scheduled to work when not using progressive
* noise floor. */
float adaptive_sampling_threshold = 0.0f;
bool last_work_tile_was_denoised = false;
bool tile_result_was_written = false;
bool postprocess_work_scheduled = false;
bool full_frame_work_scheduled = false;
bool full_frame_was_written = false;
bool path_trace_finished = false;
bool time_limit_reached = false;
/* Time at which rendering started and finished. */
double start_render_time = 0.0;
double end_render_time = 0.0;
/* Measured occupancy of the render devices measured normalized to the number of samples.
*
* In a way it is "trailing": when scheduling new work this occupancy is measured when the
* previous work was rendered. */
int occupancy_num_samples = 0;
float occupancy = 1.0f;
} state_;
/* Timing of tasks which were performed at the very first render work at 100% of the
* resolution. This timing information is used to estimate resolution divider for fats
* navigation. */
struct {
double path_trace_per_sample;
double denoise_time;
double display_update_time;
} first_render_time_;
TimeWithAverage path_trace_time_;
TimeWithAverage adaptive_filter_time_;
TimeWithAverage denoise_time_;
TimeWithAverage display_update_time_;
TimeWithAverage rebalance_time_;
TimeWithAverage volume_guiding_denoise_time_;
/* Whether cryptomatte-related work will be scheduled. */
bool need_schedule_cryptomatte_ = false;
/* Whether to schedule device load rebalance works.
* Rebalancing requires some special treatment for update intervals and such, so if it's known
* that the rebalance will be ignored (due to single-device rendering i.e.) is better to fully
* ignore rebalancing logic. */
bool need_schedule_rebalance_works_ = false;
/* Path tracing work will be scheduled for samples from within
* [sample_offset_, sample_offset_ + num_samples_ - 1] range, inclusively. */
int sample_offset_ = 0;
int num_samples_ = 0;
/* Limit in seconds for how long path tracing is allowed to happen.
* Zero means no limit is applied. */
double time_limit_ = 0.0;
/* Headless rendering without interface. */
bool headless_;
/* Background (offline) rendering. */
bool background_;
/* Pixel size is used to force lower resolution render for final pass. Useful for retina or other
* types of hi-dpi displays. */
int pixel_size_ = 1;
TileManager &tile_manager_;
BufferParams buffer_params_;
DenoiseParams denoiser_params_;
AdaptiveSampling adaptive_sampling_;
/* Progressively lower adaptive sampling threshold level, keeping the image at a uniform noise
* level. */
bool use_progressive_noise_floor_ = false;
/* Default value for the resolution divider which will be used when there is no render time
* information available yet.
* It is also what defines the upper limit of the automatically calculated resolution divider. */
int default_start_resolution_divider_ = 1;
/* Initial resolution divider which will be used on render scheduler reset. */
int start_resolution_divider_ = 0;
/* Calculate smallest resolution divider which will bring down actual rendering time below the
* desired one. This call assumes linear dependency of render time from number of pixels
* (quadratic dependency from the resolution divider): resolution divider of 2 brings render time
* down by a factor of 4. */
int calculate_resolution_divider_for_time(const double desired_time, const double actual_time);
/* If the number of samples per rendering progression should be limited because of path guiding
* being activated or is still inside its training phase */
int limit_samples_per_update_ = 0;
};
int calculate_resolution_divider_for_resolution(const int width,
const int height,
const int resolution);
int calculate_resolution_for_divider(const int width,
const int height,
const int resolution_divider);
CCL_NAMESPACE_END

View File

@@ -0,0 +1,206 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "kernel/device/cpu/globals.h"
#include "integrator/shader_eval.h"
#include "device/device.h"
#include "device/queue.h"
#include "device/cpu/kernel.h"
#include "util/log.h"
#include "util/progress.h"
#include "util/scoped_defer.h"
#include "util/tbb.h"
CCL_NAMESPACE_BEGIN
ShaderEval::ShaderEval(Device *device, Progress &progress) : device_(device), progress_(progress)
{
DCHECK_NE(device_, nullptr);
}
bool ShaderEval::eval(const ShaderEvalType type,
const int max_num_inputs,
const int num_channels,
const std::function<int(device_vector<KernelShaderEvalInput> &)> &fill_input,
const std::function<void(device_vector<float> &)> &read_output)
{
bool first_device = true;
bool success = true;
device_->foreach_device([&](Device *device) {
if (!first_device) {
LOG_DEBUG << "Multi-devices are not yet fully implemented, will evaluate shader on a "
"single device.";
return;
}
first_device = false;
device_vector<KernelShaderEvalInput> input(device, "ShaderEval input", MEM_READ_ONLY);
device_vector<float> output(device, "ShaderEval output", MEM_READ_WRITE);
/* Allocate and copy device buffers. */
DCHECK_EQ(input.device, device);
DCHECK_EQ(output.device, device);
DCHECK_LE(output.size(), input.size());
input.alloc(max_num_inputs);
int const num_points = fill_input(input);
if (num_points == 0) {
return;
}
input.copy_to_device();
output.alloc(num_points * num_channels);
output.zero_to_device();
/* Evaluate on CPU or GPU. */
success = (device->info.type == DEVICE_CPU) ?
eval_cpu(device, type, input, output, num_points) :
eval_gpu(device, type, input, output, num_points);
/* Copy data back from device if not canceled. */
if (success) {
output.copy_from_device(0, 1, output.size());
read_output(output);
}
input.free();
output.free();
});
return success;
}
bool ShaderEval::eval_cpu(Device *device,
const ShaderEvalType type,
device_vector<KernelShaderEvalInput> &input,
device_vector<float> &output,
const int64_t work_size)
{
vector<ThreadKernelGlobalsCPU> *kernel_thread_globals =
device->acquire_cpu_kernel_thread_globals();
SCOPED_DEFER(device->release_cpu_kernel_thread_globals());
/* Find required kernel function. */
const CPUKernels &kernels = Device::get_cpu_kernels();
/* Simple parallel_for over all work items. */
KernelShaderEvalInput *input_data = input.data();
float *output_data = output.data();
bool success = true;
tbb::task_arena local_arena(device->info.cpu_threads);
local_arena.execute([&]() {
parallel_for(int64_t(0), work_size, [&](int64_t work_index) {
/* TODO: is this fast enough? */
if (progress_.get_cancel()) {
success = false;
return;
}
const int thread_index = tbb::this_task_arena::current_thread_index();
const ThreadKernelGlobalsCPU *kg = &(*kernel_thread_globals)[thread_index];
switch (type) {
case SHADER_EVAL_DISPLACE:
kernels.shader_eval_displace(kg, input_data, output_data, work_index);
break;
case SHADER_EVAL_BACKGROUND:
kernels.shader_eval_background(kg, input_data, output_data, work_index);
break;
case SHADER_EVAL_CURVE_SHADOW_TRANSPARENCY:
kernels.shader_eval_curve_shadow_transparency(kg, input_data, output_data, work_index);
break;
case SHADER_EVAL_VOLUME_DENSITY:
kernels.shader_eval_volume_density(kg, input_data, output_data, work_index);
break;
}
});
});
return success;
}
bool ShaderEval::eval_gpu(Device *device,
const ShaderEvalType type,
device_vector<KernelShaderEvalInput> &input,
device_vector<float> &output,
const int64_t work_size)
{
/* Find required kernel function. */
DeviceKernel kernel;
switch (type) {
case SHADER_EVAL_DISPLACE:
kernel = DEVICE_KERNEL_SHADER_EVAL_DISPLACE;
break;
case SHADER_EVAL_BACKGROUND:
kernel = DEVICE_KERNEL_SHADER_EVAL_BACKGROUND;
break;
case SHADER_EVAL_CURVE_SHADOW_TRANSPARENCY:
kernel = DEVICE_KERNEL_SHADER_EVAL_CURVE_SHADOW_TRANSPARENCY;
break;
case SHADER_EVAL_VOLUME_DENSITY:
kernel = DEVICE_KERNEL_SHADER_EVAL_VOLUME_DENSITY;
};
/* Create device queue. */
unique_ptr<DeviceQueue> queue = device->gpu_queue_create();
queue->init_execution();
device_vector<uint> cache_miss(device, "ShaderEval cache_miss", MEM_READ_WRITE);
cache_miss.alloc(1);
cache_miss[0] = false;
/* Execute work on GPU in chunk, so we can cancel.
* TODO: query appropriate size from device. */
const int32_t chunk_size = 1 << 21;
const device_ptr d_input = input.device_pointer;
device_ptr d_output = output.device_pointer;
assert(work_size <= 0x7fffffff);
for (int32_t d_offset = 0; d_offset < int32_t(work_size); d_offset += chunk_size) {
int32_t d_work_size = std::min(chunk_size, int32_t(work_size) - d_offset);
do {
if (device->have_error() || progress_.get_cancel()) {
return false;
}
if (cache_miss[0]) {
/* Update image cache if needed. */
device->image_load_requested_gpu(*queue);
cache_miss[0] = false;
if (device->have_error() || progress_.get_cancel()) {
return false;
}
}
/* Execute shaders. */
queue->copy_to_device(cache_miss);
const DeviceKernelArguments args(
&d_input, &d_output, &cache_miss.device_pointer, &d_offset, &d_work_size);
queue->enqueue(kernel, d_work_size, args);
queue->copy_from_device(cache_miss);
if (!queue->synchronize()) {
return false;
}
/* Keep trying until there is no more cache miss. We could try to only re-execute
* items with cache misses, however all work items use the same shader and so
* likely the same tiled images textures. So it's unlikely for there to be much
* divergence as probably all or none have a cache miss. */
} while (cache_miss[0]);
}
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,54 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <functional>
#include "device/memory.h"
#include "kernel/types.h"
CCL_NAMESPACE_BEGIN
class Device;
class Progress;
enum ShaderEvalType {
SHADER_EVAL_DISPLACE,
SHADER_EVAL_BACKGROUND,
SHADER_EVAL_CURVE_SHADOW_TRANSPARENCY,
SHADER_EVAL_VOLUME_DENSITY,
};
/* ShaderEval class performs shader evaluation for background light and displacement. */
class ShaderEval {
public:
ShaderEval(Device *device, Progress &progress);
/* Evaluate shader at points specified by KernelShaderEvalInput and write out
* RGBA colors to output. */
bool eval(const ShaderEvalType type,
const int max_num_inputs,
const int num_channels,
const std::function<int(device_vector<KernelShaderEvalInput> &)> &fill_input,
const std::function<void(device_vector<float> &)> &read_output);
protected:
bool eval_cpu(Device *device,
const ShaderEvalType type,
device_vector<KernelShaderEvalInput> &input,
device_vector<float> &output,
const int64_t work_size);
bool eval_gpu(Device *device,
const ShaderEvalType type,
device_vector<KernelShaderEvalInput> &input,
device_vector<float> &output,
const int64_t work_size);
Device *device_;
Progress &progress_;
};
CCL_NAMESPACE_END

View File

@@ -0,0 +1,107 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/tile.h"
#include "util/log.h"
#include "util/math_base.h"
#include <ostream>
CCL_NAMESPACE_BEGIN
std::ostream &operator<<(std::ostream &os, const TileSize &tile_size)
{
os << "size: (" << tile_size.width << ", " << tile_size.height << ")";
os << ", num_samples: " << tile_size.num_samples;
return os;
}
ccl_device_inline uint round_down_to_power_of_two(const uint x)
{
if (is_power_of_two(x)) {
return x;
}
return prev_power_of_two(x);
}
ccl_device_inline uint round_up_to_power_of_two(const uint x)
{
if (is_power_of_two(x)) {
return x;
}
return next_power_of_two(x);
}
TileSize tile_calculate_best_size(const bool accel_rt,
const int2 &image_size,
const int num_samples,
const int max_num_path_states,
const float scrambling_distance)
{
if (max_num_path_states == 1) {
/* Simple case: avoid any calculation, which could cause rounding issues. */
return TileSize(1, 1, 1);
}
const int64_t num_pixels = image_size.x * image_size.y;
const int64_t num_pixel_samples = num_pixels * num_samples;
if (max_num_path_states >= num_pixel_samples) {
/* Image fully fits into the state (could be border render, for example). */
return TileSize(image_size.x, image_size.y, num_samples);
}
/* The idea here is to keep number of samples per tile as much as possible to improve coherency
* across threads.
*
* Some general ideas:
* - Prefer smaller tiles with more samples, which improves spatial coherency of paths.
* - Keep values a power of two, for more integer fit into the maximum number of paths. */
TileSize tile_size;
const int num_path_states_per_sample = max_num_path_states / num_samples;
if (scrambling_distance < 0.9f && accel_rt) {
/* Prefer large tiles for scrambling distance, bounded by max num path states. */
tile_size.width = min(image_size.x, max_num_path_states);
tile_size.height = min(image_size.y, max(max_num_path_states / tile_size.width, 1));
}
else {
/* Calculate tile size as if it is the most possible one to fit an entire range of samples.
* The idea here is to keep tiles as small as possible, and keep device occupied by scheduling
* multiple tiles with the same coordinates rendering different samples. */
if (num_path_states_per_sample != 0) {
tile_size.width = round_down_to_power_of_two(lround(sqrt(num_path_states_per_sample)));
tile_size.height = tile_size.width;
}
else {
tile_size.width = tile_size.height = 1;
}
}
if (num_samples == 1) {
tile_size.num_samples = 1;
}
else {
/* Heuristic here is to have more uniform division of the sample range: for example prefer
* [32 <38 times>, 8] over [1024, 200]. This allows to greedily add more tiles early on. */
tile_size.num_samples = min(round_up_to_power_of_two(lround(sqrt(num_samples / 2))),
static_cast<uint>(num_samples));
const int tile_area = tile_size.width * tile_size.height;
tile_size.num_samples = min(tile_size.num_samples, max_num_path_states / tile_area);
}
DCHECK_GE(tile_size.width, 1);
DCHECK_GE(tile_size.height, 1);
DCHECK_GE(tile_size.num_samples, 1);
DCHECK_LE(tile_size.width * tile_size.height * tile_size.num_samples, max_num_path_states);
return tile_size;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,46 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include <iosfwd>
#include "util/types_int2.h"
CCL_NAMESPACE_BEGIN
struct TileSize {
TileSize() = default;
TileSize(const int width, const int height, const int num_samples)
: width(width), height(height), num_samples(num_samples)
{
}
bool operator==(const TileSize &other) const
{
return width == other.width && height == other.height && num_samples == other.num_samples;
}
bool operator!=(const TileSize &other) const
{
return !(*this == other);
}
int width = 0, height = 0;
int num_samples = 0;
};
std::ostream &operator<<(std::ostream &os, const TileSize &tile_size);
/* Calculate tile size which is best suitable for rendering image of a given size with given number
* of active path states.
* Will attempt to provide best guess to keep path tracing threads of a device as localized as
* possible, and have as many threads active for every tile as possible. */
TileSize tile_calculate_best_size(const bool accel_rt,
const int2 &image_size,
const int num_samples,
const int max_num_path_states,
const float scrambling_distance);
CCL_NAMESPACE_END

View File

@@ -0,0 +1,88 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/work_balancer.h"
#include "util/math_base.h"
CCL_NAMESPACE_BEGIN
void work_balance_do_initial(vector<WorkBalanceInfo> &work_balance_infos)
{
const int num_infos = work_balance_infos.size();
if (num_infos == 1) {
work_balance_infos[0].weight = 1.0;
return;
}
if (num_infos == 0) {
return;
}
/* There is no statistics available, so start with an equal distribution. */
const double weight = 1.0 / num_infos;
for (WorkBalanceInfo &balance_info : work_balance_infos) {
balance_info.weight = weight;
}
}
static double calculate_total_time(const vector<WorkBalanceInfo> &work_balance_infos)
{
double total_time = 0;
for (const WorkBalanceInfo &info : work_balance_infos) {
total_time += info.time_spent;
}
return total_time;
}
/* The balance is based on equalizing time which devices spent performing a task. Assume that
* average of the observed times is usable for estimating whether more or less work is to be
* scheduled, and how difference in the work scheduling is needed. */
bool work_balance_do_rebalance(vector<WorkBalanceInfo> &work_balance_infos)
{
const int num_infos = work_balance_infos.size();
const double total_time = calculate_total_time(work_balance_infos);
const double time_average = total_time / num_infos;
double total_weight = 0;
vector<double> new_weights;
new_weights.reserve(num_infos);
/* Equalize the overall average time. This means that we don't make it so every work will perform
* amount of work based on the current average, but that after the weights changes the time will
* equalize.
* Can think of it that if one of the devices is 10% faster than another, then one device needs
* to do 5% less of the current work, and another needs to do 5% more. */
const double lerp_weight = 1.0 / num_infos;
bool has_big_difference = false;
for (const WorkBalanceInfo &info : work_balance_infos) {
const double time_target = mix(info.time_spent, time_average, lerp_weight);
const double new_weight = info.weight * time_target / info.time_spent;
new_weights.push_back(new_weight);
total_weight += new_weight;
if (std::fabs(1.0 - time_target / time_average) > 0.02) {
has_big_difference = true;
}
}
if (!has_big_difference) {
return false;
}
const double total_weight_inv = 1.0 / total_weight;
for (int i = 0; i < num_infos; ++i) {
WorkBalanceInfo &info = work_balance_infos[i];
info.weight = new_weights[i] * total_weight_inv;
info.time_spent = 0;
}
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "util/vector.h"
CCL_NAMESPACE_BEGIN
struct WorkBalanceInfo {
/* Time spent performing corresponding work. */
double time_spent = 0;
/* Average occupancy of the device while performing the work. */
float occupancy = 1.0f;
/* Normalized weight, which is ready to be used for work balancing (like calculating fraction of
* the big tile which is to be rendered on the device). */
double weight = 1.0;
};
/* Balance work for an initial render integration, before any statistics is known. */
void work_balance_do_initial(vector<WorkBalanceInfo> &work_balance_infos);
/* Rebalance work after statistics has been accumulated.
* Returns true if the balancing did change. */
bool work_balance_do_rebalance(vector<WorkBalanceInfo> &work_balance_infos);
CCL_NAMESPACE_END

View File

@@ -0,0 +1,139 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#include "integrator/work_tile_scheduler.h"
#include "device/queue.h"
#include "integrator/tile.h"
#include "session/buffers.h"
#include "util/log.h"
CCL_NAMESPACE_BEGIN
WorkTileScheduler::WorkTileScheduler() = default;
void WorkTileScheduler::set_accelerated_rt(bool accelerated_rt)
{
accelerated_rt_ = accelerated_rt;
}
void WorkTileScheduler::set_max_num_path_states(const int max_num_path_states)
{
max_num_path_states_ = max_num_path_states;
}
void WorkTileScheduler::reset(const BufferParams &buffer_params,
const int sample_start,
const int samples_num,
const int sample_offset,
const float scrambling_distance)
{
/* Image buffer parameters. */
image_full_offset_px_.x = buffer_params.full_x;
image_full_offset_px_.y = buffer_params.full_y;
image_size_px_ = make_int2(buffer_params.width, buffer_params.height);
scrambling_distance_ = scrambling_distance;
offset_ = buffer_params.offset;
stride_ = buffer_params.stride;
/* Samples parameters. */
sample_start_ = sample_start;
samples_num_ = samples_num;
sample_offset_ = sample_offset;
/* Initialize new scheduling. */
reset_scheduler_state();
}
void WorkTileScheduler::reset_scheduler_state()
{
tile_size_ = tile_calculate_best_size(
accelerated_rt_, image_size_px_, samples_num_, max_num_path_states_, scrambling_distance_);
const int num_path_states_in_tile = tile_size_.width * tile_size_.height *
tile_size_.num_samples;
if (num_path_states_in_tile == 0) {
LOG_DEBUG << "Will not schedule any tiles: no work remained for the device";
num_tiles_x_ = 0;
num_tiles_y_ = 0;
num_tiles_per_sample_range_ = 0;
}
else {
const int num_tiles = max_num_path_states_ / num_path_states_in_tile;
LOG_DEBUG << "Will schedule " << num_tiles << " tiles of " << tile_size_;
/* The logging is based on multiple tiles scheduled, ignoring overhead of multi-tile
* scheduling and purely focusing on the number of used path states. */
LOG_DEBUG << "Number of unused path states: "
<< max_num_path_states_ - num_tiles * num_path_states_in_tile;
num_tiles_x_ = divide_up(image_size_px_.x, tile_size_.width);
num_tiles_y_ = divide_up(image_size_px_.y, tile_size_.height);
num_tiles_per_sample_range_ = divide_up(samples_num_, tile_size_.num_samples);
}
total_tiles_num_ = num_tiles_x_ * num_tiles_y_;
next_work_index_ = 0;
total_work_size_ = total_tiles_num_ * num_tiles_per_sample_range_;
}
bool WorkTileScheduler::get_work(KernelWorkTile *work_tile_, const int max_work_size)
{
/* Note that the `max_work_size` can be higher than the `max_num_path_states_`: this is because
* the path trace work can decide to use smaller tile sizes and greedily schedule multiple tiles,
* improving overall device occupancy.
* So the `max_num_path_states_` is a "scheduling unit", and the `max_work_size` is a "scheduling
* limit". */
DCHECK_NE(max_num_path_states_, 0);
const int work_index = next_work_index_++;
if (work_index >= total_work_size_) {
return false;
}
const int sample_range_index = work_index % num_tiles_per_sample_range_;
const int start_sample = sample_range_index * tile_size_.num_samples;
const int tile_index = work_index / num_tiles_per_sample_range_;
const int tile_y = tile_index / num_tiles_x_;
const int tile_x = tile_index - tile_y * num_tiles_x_;
KernelWorkTile work_tile;
work_tile.x = tile_x * tile_size_.width;
work_tile.y = tile_y * tile_size_.height;
work_tile.w = tile_size_.width;
work_tile.h = tile_size_.height;
work_tile.start_sample = sample_start_ + start_sample;
work_tile.num_samples = min(tile_size_.num_samples, samples_num_ - start_sample);
work_tile.sample_offset = sample_offset_;
work_tile.offset = offset_;
work_tile.stride = stride_;
work_tile.w = min(work_tile.w, image_size_px_.x - work_tile.x);
work_tile.h = min(work_tile.h, image_size_px_.y - work_tile.y);
work_tile.x += image_full_offset_px_.x;
work_tile.y += image_full_offset_px_.y;
const int tile_work_size = work_tile.w * work_tile.h * work_tile.num_samples;
DCHECK_GT(tile_work_size, 0);
if (max_work_size && tile_work_size > max_work_size) {
/* The work did not fit into the requested limit of the work size. Unschedule the tile,
* so it can be picked up again later. */
next_work_index_--;
return false;
}
*work_tile_ = work_tile;
return true;
}
CCL_NAMESPACE_END

View File

@@ -0,0 +1,101 @@
/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation
*
* SPDX-License-Identifier: Apache-2.0 */
#pragma once
#include "integrator/tile.h"
#include "util/types_int2.h"
CCL_NAMESPACE_BEGIN
class BufferParams;
struct KernelWorkTile;
/* Scheduler of device work tiles.
* Takes care of feeding multiple devices running in parallel a work which needs to be done. */
class WorkTileScheduler {
public:
WorkTileScheduler();
/* To indicate if there is accelerated RT support. */
void set_accelerated_rt(bool accelerated_rt);
/* MAximum path states which are allowed to be used by a single scheduled work tile.
*
* Affects the scheduled work size: the work size will be as big as possible, but will not exceed
* this number of states. */
void set_max_num_path_states(const int max_num_path_states);
/* Scheduling will happen for pixels within a big tile denotes by its parameters. */
void reset(const BufferParams &buffer_params,
const int sample_start,
const int samples_num,
const int sample_offset,
float scrambling_distance);
/* Get work for a device.
* Returns true if there is still work to be done and initialize the work tile to all
* parameters of this work. If there is nothing remaining to be done, returns false and the
* work tile is kept unchanged.
*
* Optionally pass max_work_size to do nothing if there is no tile small enough. */
bool get_work(KernelWorkTile *work_tile, const int max_work_size = 0);
protected:
void reset_scheduler_state();
/* Used to indicate if there is accelerated ray tracing. */
bool accelerated_rt_ = false;
/* Maximum allowed path states to be used.
*
* TODO(sergey): Naming can be improved. The fact that this is a limiting factor based on the
* number of path states is kind of a detail. Is there a more generic term from the scheduler
* point of view? */
int max_num_path_states_ = 0;
/* Offset in pixels within a global buffer. */
int2 image_full_offset_px_ = make_int2(0, 0);
/* dimensions of the currently rendering image in pixels. */
int2 image_size_px_ = make_int2(0, 0);
/* Offset and stride of the buffer within which scheduling is happening.
* Will be passed over to the KernelWorkTile. */
int offset_, stride_;
/* Scrambling Distance requires adapted tile size */
float scrambling_distance_;
/* Start sample of index and number of samples which are to be rendered.
* The scheduler will cover samples range of [start, start + num] over the entire image
* (splitting into a smaller work tiles). */
int sample_start_ = 0;
int samples_num_ = 0;
int sample_offset_ = 0;
/* Tile size which be scheduled for rendering. */
TileSize tile_size_;
/* Number of tiles in X and Y axis of the image. */
int num_tiles_x_, num_tiles_y_;
/* Total number of tiles on the image.
* Pre-calculated as `num_tiles_x_ * num_tiles_y_` and re-used in the `get_work()`.
*
* TODO(sergey): Is this an over-optimization? Maybe it's unmeasurable to calculate the value
* in the `get_work()`? */
int total_tiles_num_ = 0;
/* In the case when the number of samples in the `tile_size_` is lower than samples_num_ denotes
* how many tiles are to be "stacked" to cover the entire requested range of samples. */
int num_tiles_per_sample_range_ = 0;
int next_work_index_ = 0;
int total_work_size_ = 0;
};
CCL_NAMESPACE_END