WIP: Fix: Respect Blender Cycles setting for GPU denoising #118841

Draft
Nikita Sirgienko wants to merge 1 commits from Sirgienko/blender:address_gpu_denoising_settings_respect into main

When changing the target branch, be careful to rebase the branch in your fork to match. See documentation.
14 changed files with 266 additions and 119 deletions

View File

@ -223,7 +223,7 @@ def enum_openimagedenoise_denoiser(self, context):
def enum_optix_denoiser(self, context):
if not context or bool(context.preferences.addons[__package__].preferences.get_devices_for_type('OPTIX')):
return [('OPTIX', "OptiX", "Use the OptiX AI denoiser with GPU acceleration, only available on NVIDIA GPUs", 2)]
return [('OPTIX', "OptiX", "Use the OptiX AI denoiser with GPU acceleration, only available on NVIDIA GPUs when configured in the system tab in the user preferences", 2)]
return []
@ -354,7 +354,7 @@ class CyclesRenderSettings(bpy.types.PropertyGroup):
)
denoising_use_gpu: BoolProperty(
name="Denoise on GPU",
description="Perform denoising on GPU devices, if available. This is significantly faster than on CPU, but requires additional GPU memory. When large scenes need more GPU memory, this option can be disabled",
description="Perform denoising on GPU devices configured in the system tab in the user preferences. This is significantly faster than on CPU, but requires additional GPU memory. When large scenes need more GPU memory, this option can be disabled",
default=False,
)
@ -389,7 +389,7 @@ class CyclesRenderSettings(bpy.types.PropertyGroup):
)
preview_denoising_use_gpu: BoolProperty(
name="Denoise Preview on GPU",
description="Perform denoising on GPU devices, if available. This is significantly faster than on CPU, but requires additional GPU memory. When large scenes need more GPU memory, this option can be disabled",
description="Perform denoising on GPU devices configured in the system tab in the user preferences. This is significantly faster than on CPU, but requires additional GPU memory. When large scenes need more GPU memory, this option can be disabled",
default=True,
)
@ -1608,6 +1608,8 @@ class CyclesPreferences(bpy.types.AddonPreferences):
# We need non-CPU devices, used for rendering and supporting OIDN GPU denoising
for device in _cycles.available_devices(compute_device_type):
device_type = device[1]
if device_type != compute_device_type:
continue
if device_type == 'CPU':
continue
@ -1617,6 +1619,22 @@ class CyclesPreferences(bpy.types.AddonPreferences):
return False
def has_optixdenoiser_gpu_devices(self):
import _cycles
compute_device_type = self.get_compute_device_type()
# We need any OptiX devices, used for rendering
for device in _cycles.available_devices(compute_device_type):
device_type = device[1]
if device_type != compute_device_type:
continue
has_device_optixdenoiser_support = device[6]
if has_device_optixdenoiser_support and self.find_existing_device_entry(device).use:
return True
return False
def _draw_devices(self, layout, device_type, devices):
box = layout.box()

View File

@ -143,6 +143,28 @@ def show_device_active(context):
return True
return backend_has_active_gpu(context)
def show_preview_denoise_active(context):
cscene = context.scene.cycles
if not cscene.use_preview_denoising:
return False
if cscene.preview_denoiser == 'OPTIX':
return has_optixdenoiser_gpu_devices(context)
# OIDN is always available, thanks to CPU support
return True
def show_denoise_active(context):
cscene = context.scene.cycles
if not cscene.use_denoising:
return False
if cscene.denoiser == 'OPTIX':
return has_optixdenoiser_gpu_devices(context)
# OIDN is always available, thanks to CPU support
return True
def get_effective_preview_denoiser(context):
scene = context.scene
@ -160,6 +182,9 @@ def get_effective_preview_denoiser(context):
def has_oidn_gpu_devices(context):
return context.preferences.addons[__package__].preferences.has_oidn_gpu_devices()
def has_optixdenoiser_gpu_devices(context):
return context.preferences.addons[__package__].preferences.has_optixdenoiser_gpu_devices()
def use_mnee(context):
# The MNEE kernel doesn't compile on macOS < 13.
@ -231,7 +256,11 @@ class CYCLES_RENDER_PT_sampling_viewport_denoise(CyclesButtonsPanel, Panel):
col = layout.column()
col.active = cscene.use_preview_denoising
col.prop(cscene, "preview_denoiser", text="Denoiser")
sub = col.column()
sub.active = show_preview_denoise_active(context)
sub.prop(cscene, "preview_denoiser", text="Denoiser")
col.prop(cscene, "preview_denoising_input_passes", text="Passes")
effective_preview_denoiser = get_effective_preview_denoiser(context)
@ -242,7 +271,7 @@ class CYCLES_RENDER_PT_sampling_viewport_denoise(CyclesButtonsPanel, Panel):
if effective_preview_denoiser == 'OPENIMAGEDENOISE':
row = col.row()
row.active = not use_cpu(context) and has_oidn_gpu_devices(context)
row.active = has_oidn_gpu_devices(context)
row.prop(cscene, "preview_denoising_use_gpu", text="Use GPU")
@ -299,14 +328,18 @@ class CYCLES_RENDER_PT_sampling_render_denoise(CyclesButtonsPanel, Panel):
col = layout.column()
col.active = cscene.use_denoising
col.prop(cscene, "denoiser", text="Denoiser")
sub = col.column()
sub.active = show_denoise_active(context)
sub.prop(cscene, "denoiser", text="Denoiser")
col.prop(cscene, "denoising_input_passes", text="Passes")
if cscene.denoiser == 'OPENIMAGEDENOISE':
col.prop(cscene, "denoising_prefilter", text="Prefilter")
if cscene.denoiser == 'OPENIMAGEDENOISE':
row = col.row()
row.active = not use_cpu(context) and has_oidn_gpu_devices(context)
row.active = has_oidn_gpu_devices(context)
row.prop(cscene, "denoising_use_gpu", text="Use GPU")

View File

@ -61,84 +61,8 @@ void static adjust_device_info_from_preferences(DeviceInfo &info, PointerRNA cpr
}
}
DeviceInfo blender_device_info(BL::Preferences &b_preferences,
BL::Scene &b_scene,
bool background,
bool preview)
void static adjust_device_info(DeviceInfo &device, PointerRNA cpreferences, bool preview)
{
PointerRNA cscene = RNA_pointer_get(&b_scene.ptr, "cycles");
/* Find cycles preferences. */
PointerRNA cpreferences;
for (BL::Addon &b_addon : b_preferences.addons) {
if (b_addon.module() == "cycles") {
cpreferences = b_addon.preferences().ptr;
break;
}
}
/* Default to CPU device. */
DeviceInfo device = Device::available_devices(DEVICE_MASK_CPU).front();
if (BlenderSession::device_override != DEVICE_MASK_ALL) {
vector<DeviceInfo> devices = Device::available_devices(BlenderSession::device_override);
if (devices.empty()) {
device = Device::dummy_device("Found no Cycles device of the specified type");
}
else {
int threads = blender_device_threads(b_scene);
device = Device::get_multi_device(devices, threads, background);
}
}
else if (get_enum(cscene, "device") == 1) {
/* Test if we are using GPU devices. */
ComputeDevice compute_device = (ComputeDevice)get_enum(
cpreferences, "compute_device_type", COMPUTE_DEVICE_NUM, COMPUTE_DEVICE_CPU);
if (compute_device != COMPUTE_DEVICE_CPU) {
/* Query GPU devices with matching types. */
uint mask = DEVICE_MASK_CPU;
if (compute_device == COMPUTE_DEVICE_CUDA) {
mask |= DEVICE_MASK_CUDA;
}
else if (compute_device == COMPUTE_DEVICE_OPTIX) {
mask |= DEVICE_MASK_OPTIX;
}
else if (compute_device == COMPUTE_DEVICE_HIP) {
mask |= DEVICE_MASK_HIP;
}
else if (compute_device == COMPUTE_DEVICE_METAL) {
mask |= DEVICE_MASK_METAL;
}
else if (compute_device == COMPUTE_DEVICE_ONEAPI) {
mask |= DEVICE_MASK_ONEAPI;
}
vector<DeviceInfo> devices = Device::available_devices(mask);
/* Match device preferences and available devices. */
vector<DeviceInfo> used_devices;
RNA_BEGIN (&cpreferences, device, "devices") {
if (get_boolean(device, "use")) {
string id = get_string(device, "id");
foreach (DeviceInfo &info, devices) {
if (info.id == id) {
used_devices.push_back(info);
break;
}
}
}
}
RNA_END;
if (!used_devices.empty()) {
int threads = blender_device_threads(b_scene);
device = Device::get_multi_device(used_devices, threads, background);
}
/* Else keep using the CPU device that was set before. */
}
}
adjust_device_info_from_preferences(device, cpreferences);
foreach (DeviceInfo &info, device.multi_devices) {
adjust_device_info_from_preferences(info, cpreferences);
@ -162,6 +86,106 @@ DeviceInfo blender_device_info(BL::Preferences &b_preferences,
KERNEL_OPTIMIZATION_NUM_LEVELS,
KERNEL_OPTIMIZATION_LEVEL_FULL);
}
}
DeviceInfo blender_device_info(BL::Preferences &b_preferences,
BL::Scene &b_scene,
bool background,
bool preview,
DeviceInfo &preferences_device)
{
PointerRNA cscene = RNA_pointer_get(&b_scene.ptr, "cycles");
/* Find cycles preferences. */
PointerRNA cpreferences;
for (BL::Addon &b_addon : b_preferences.addons) {
if (b_addon.module() == "cycles") {
cpreferences = b_addon.preferences().ptr;
break;
}
}
/* Default to CPU device. */
DeviceInfo cpu_device = Device::available_devices(DEVICE_MASK_CPU).front();
/* Device, which is choosen in the Blender Preferences. */
preferences_device = DeviceInfo();
/* Test if we are using GPU devices. */
ComputeDevice compute_device = (ComputeDevice)get_enum(
cpreferences, "compute_device_type", COMPUTE_DEVICE_NUM, COMPUTE_DEVICE_CPU);
if (compute_device != COMPUTE_DEVICE_CPU) {
/* Query GPU devices with matching types. */
uint mask = DEVICE_MASK_CPU;
if (compute_device == COMPUTE_DEVICE_CUDA) {
mask |= DEVICE_MASK_CUDA;
}
else if (compute_device == COMPUTE_DEVICE_OPTIX) {
mask |= DEVICE_MASK_OPTIX;
}
else if (compute_device == COMPUTE_DEVICE_HIP) {
mask |= DEVICE_MASK_HIP;
}
else if (compute_device == COMPUTE_DEVICE_METAL) {
mask |= DEVICE_MASK_METAL;
}
else if (compute_device == COMPUTE_DEVICE_ONEAPI) {
mask |= DEVICE_MASK_ONEAPI;
}
vector<DeviceInfo> devices = Device::available_devices(mask);
/* Match device preferences and available devices. */
vector<DeviceInfo> used_devices;
RNA_BEGIN (&cpreferences, device, "devices") {
if (get_boolean(device, "use")) {
string id = get_string(device, "id");
foreach (DeviceInfo &info, devices) {
if (info.id == id) {
used_devices.push_back(info);
break;
}
}
}
}
RNA_END;
if (!used_devices.empty()) {
int threads = blender_device_threads(b_scene);
preferences_device = Device::get_multi_device(used_devices, threads, background);
}
}
else {
preferences_device = cpu_device;
}
adjust_device_info(preferences_device, cpreferences, preview);
adjust_device_info(cpu_device, cpreferences, preview);
/* Device, which will be used, according to Settings, Scene preferences and command line
* parameters. */
DeviceInfo device;
if (BlenderSession::device_override != DEVICE_MASK_ALL) {
vector<DeviceInfo> devices = Device::available_devices(BlenderSession::device_override);
if (devices.empty()) {
device = Device::dummy_device("Found no Cycles device of the specified type");
}
else {
int threads = blender_device_threads(b_scene);
device = Device::get_multi_device(devices, threads, background);
}
adjust_device_info(device, cpreferences, preview);
}
else {
/* 1 is a "GPU compute" in properties.py for Scene settings. */
if (get_enum(cscene, "device") == 1) {
device = preferences_device;
}
else {
device = cpu_device;
}
}
return device;
}

View File

@ -17,11 +17,14 @@ CCL_NAMESPACE_BEGIN
/* Get number of threads to use for rendering. */
int blender_device_threads(BL::Scene &b_scene);
/* Convert Blender settings to device specification. */
/* Convert Blender settings to device specification. In addition, preferences_device contains the
* device chosen in Cycles global preferences, which is useful for the denoiser device selection.
*/
DeviceInfo blender_device_info(BL::Preferences &b_preferences,
BL::Scene &b_scene,
bool background,
bool preview);
bool preview,
DeviceInfo &preferences_device);
CCL_NAMESPACE_END

View File

@ -417,7 +417,7 @@ static PyObject *available_devices_func(PyObject * /*self*/, PyObject *args)
for (size_t i = 0; i < devices.size(); i++) {
DeviceInfo &device = devices[i];
string type_name = Device::string_from_type(device.type);
PyObject *device_tuple = PyTuple_New(6);
PyObject *device_tuple = PyTuple_New(7);
PyTuple_SET_ITEM(device_tuple, 0, pyunicode_from_string(device.description.c_str()));
PyTuple_SET_ITEM(device_tuple, 1, pyunicode_from_string(type_name.c_str()));
PyTuple_SET_ITEM(device_tuple, 2, pyunicode_from_string(device.id.c_str()));
@ -425,6 +425,7 @@ static PyObject *available_devices_func(PyObject * /*self*/, PyObject *args)
PyTuple_SET_ITEM(device_tuple, 4, PyBool_FromLong(device.use_hardware_raytracing));
PyTuple_SET_ITEM(
device_tuple, 5, PyBool_FromLong(device.denoisers & DENOISER_OPENIMAGEDENOISE));
PyTuple_SET_ITEM(device_tuple, 6, PyBool_FromLong(device.denoisers & DENOISER_OPTIX));
PyTuple_SET_ITEM(ret, i, device_tuple);
}
@ -754,7 +755,9 @@ static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *key
PointerRNA sceneptr = RNA_id_pointer_create((ID *)PyLong_AsVoidPtr(pyscene));
BL::Scene b_scene(sceneptr);
DeviceInfo device = blender_device_info(b_preferences, b_scene, true, true);
DeviceInfo preferences_device;
DeviceInfo pathtrace_device = blender_device_info(
b_preferences, b_scene, true, true, preferences_device);
/* Get denoising parameters from view layer. */
PointerRNA viewlayerptr = RNA_pointer_create(
@ -790,7 +793,10 @@ static PyObject *denoise_func(PyObject * /*self*/, PyObject *args, PyObject *key
}
/* Create denoiser. */
DenoiserPipeline denoiser(device, params);
/* We are using preference device here, because path trace device will be identical to it unless
* scene is setting CPU render or command line override render device. But both of this options
* are for render, not for denoising. */
DenoiserPipeline denoiser(preferences_device, params);
denoiser.input = input;
denoiser.output = output;

View File

@ -882,7 +882,7 @@ SessionParams BlenderSync::get_session_params(BL::RenderEngine &b_engine,
/* Device */
params.threads = blender_device_threads(b_scene);
params.device = blender_device_info(
b_preferences, b_scene, params.background, b_engine.is_preview());
b_preferences, b_scene, params.background, b_engine.is_preview(), params.preferences_device);
/* samples */
int samples = get_int(cscene, "samples");

View File

@ -16,30 +16,32 @@
CCL_NAMESPACE_BEGIN
unique_ptr<Denoiser> Denoiser::create(Device *path_trace_device, const DenoiseParams &params)
unique_ptr<Denoiser> Denoiser::create(Device *denoise_device, const DenoiseParams &params)
{
DCHECK(params.use);
if (denoise_device->info.type != DEVICE_CPU) {
#ifdef WITH_OPTIX
if (params.type == DENOISER_OPTIX && Device::available_devices(DEVICE_MASK_OPTIX).size()) {
return make_unique<OptiXDenoiser>(path_trace_device, params);
}
if (params.type == DENOISER_OPTIX) {
return make_unique<OptiXDenoiser>(denoise_device, params);
}
#endif
#ifdef WITH_OPENIMAGEDENOISE
/* If available and allowed, then we will use OpenImageDenoise on GPU, otherwise on CPU. */
if (params.type == DENOISER_OPENIMAGEDENOISE && params.use_gpu &&
path_trace_device->info.type != DEVICE_CPU &&
OIDNDenoiserGPU::is_device_supported(path_trace_device->info))
{
return make_unique<OIDNDenoiserGPU>(path_trace_device, params);
}
/* If available and allowed, then we will use OpenImageDenoise on GPU, otherwise on CPU. */
if (params.type == DENOISER_OPENIMAGEDENOISE && params.use_gpu &&
OIDNDenoiserGPU::is_device_supported(denoise_device->info))
{
return make_unique<OIDNDenoiserGPU>(denoise_device, params);
}
#endif
}
/* Always fallback to OIDN. */
DenoiseParams oidn_params = params;
oidn_params.type = DENOISER_OPENIMAGEDENOISE;
return make_unique<OIDNDenoiser>(path_trace_device, oidn_params);
oidn_params.use_gpu = false;
return make_unique<OIDNDenoiser>(denoise_device, oidn_params);
}
Denoiser::Denoiser(Device *path_trace_device, const DenoiseParams &params)
@ -67,13 +69,23 @@ const DenoiseParams &Denoiser::get_params() const
bool Denoiser::load_kernels(Progress *progress)
{
const Device *denoiser_device = ensure_denoiser_device(progress);
Device *denoiser_device = ensure_denoiser_device(progress);
if (!denoiser_device) {
path_trace_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";
}
path_trace_device_->set_error(message);
return false;
}
VLOG_WORK << "Will denoise on " << denoiser_device->info.description << " ("
<< denoiser_device->info.id << ")";

View File

@ -33,7 +33,7 @@ class Denoiser {
* - The denoiser must be configured. This means that `params.use` must be true.
* This is checked in debug builds.
* - The device might be MultiDevice. */
static unique_ptr<Denoiser> create(Device *path_trace_device, const DenoiseParams &params);
static unique_ptr<Denoiser> create(Device *denoise_device, const DenoiseParams &params);
virtual ~Denoiser() = default;

View File

@ -22,11 +22,13 @@
CCL_NAMESPACE_BEGIN
PathTrace::PathTrace(Device *device,
Device *denoise_device,
Film *film,
DeviceScene *device_scene,
RenderScheduler &render_scheduler,
TileManager &tile_manager)
: device_(device),
denoise_device_(denoise_device),
film_(film),
device_scene_(device_scene),
render_scheduler_(render_scheduler),
@ -483,15 +485,25 @@ void PathTrace::adaptive_sample(RenderWork &render_work)
void PathTrace::set_denoiser_params(const DenoiseParams &params)
{
render_scheduler_.set_denoiser_params(params);
bool need_to_recreate_denoiser = false;
if (!params.use) {
denoiser_.reset();
return;
}
if (denoiser_) {
else if (denoiser_) {
const DenoiseParams old_denoiser_params = denoiser_->get_params();
const bool is_cpu_denoising = old_denoiser_params.type == DENOISER_OPENIMAGEDENOISE &&
old_denoiser_params.use_gpu == false;
const bool requested_gpu_denoising = params.type == DENOISER_OPTIX ||
(params.type == DENOISER_OPENIMAGEDENOISE &&
params.use_gpu == true);
if (requested_gpu_denoising && is_cpu_denoising && denoise_device_->info.type == DEVICE_CPU) {
/* It won't be possible to use GPU denoising when according to user settings we have
* only CPU as available denoising device. So we just exiting early to avoid
* unnecessary denoiser recreation or parameters update. */
return;
}
const bool is_same_denoising_device_type = old_denoiser_params.use_gpu == params.use_gpu;
/* Optix Denoiser is not supporting CPU devices, so use_gpu option is not
* shown in the UI and changes in the option value should not be checked. */
@ -499,16 +511,30 @@ void PathTrace::set_denoiser_params(const DenoiseParams &params)
(is_same_denoising_device_type || params.type == DENOISER_OPTIX))
{
denoiser_->set_params(params);
return;
}
else {
need_to_recreate_denoiser = true;
}
}
else {
/* if there is no denoiser and param.use is true, then we need to create it. */
need_to_recreate_denoiser = true;
}
denoiser_ = Denoiser::create(device_, params);
if (need_to_recreate_denoiser) {
denoiser_ = Denoiser::create(denoise_device_, params);
/* Only take into account the "immediate" cancel to have interactive rendering responding to
* navigation as quickly as possible, but allow to run denoiser after user hit Escape key while
* doing offline rendering. */
denoiser_->is_cancelled_cb = [this]() { return render_cancel_.is_requested; };
/* Only take into account the "immediate" cancel to have interactive rendering responding to
* navigation as quickly as possible, but allow to run denoiser after user hit Escape key while
* doing offline rendering. */
denoiser_->is_cancelled_cb = [this]() { return render_cancel_.is_requested; };
}
/* Use actual parameters, if available */
if (denoise_device_)
render_scheduler_.set_denoiser_params(denoiser_->get_params());
else
render_scheduler_.set_denoiser_params(params);
}
void PathTrace::set_adaptive_sampling(const AdaptiveSampling &adaptive_sampling)

View File

@ -45,6 +45,7 @@ class PathTrace {
/* Render scheduler is used to report timing information and access things like start/finish
* sample. */
PathTrace(Device *device,
Device *denoiser_device,
Film *film,
DeviceScene *device_scene,
RenderScheduler &render_scheduler,
@ -251,6 +252,10 @@ class PathTrace {
* 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_;

View File

@ -599,13 +599,13 @@ bool DenoiseImage::save_output(const string &out_filepath, string &error)
/* File pattern handling and outer loop over frames */
DenoiserPipeline::DenoiserPipeline(DeviceInfo &device_info, const DenoiseParams &params)
DenoiserPipeline::DenoiserPipeline(DeviceInfo &denoiser_device_info, const DenoiseParams &params)
{
/* Initialize task scheduler. */
TaskScheduler::init();
/* Initialize device. */
device = Device::create(device_info, stats, profiler);
device = Device::create(denoiser_device_info, stats, profiler);
device->load_kernels(KERNEL_FEATURE_DENOISING);
denoiser = Denoiser::create(device, params);

View File

@ -25,7 +25,7 @@ CCL_NAMESPACE_BEGIN
class DenoiserPipeline {
public:
DenoiserPipeline(DeviceInfo &device_info, const DenoiseParams &params);
DenoiserPipeline(DeviceInfo &denoiser_device_info, const DenoiseParams &params);
~DenoiserPipeline();
bool run();

View File

@ -50,9 +50,20 @@ Session::Session(const SessionParams &params_, const SceneParams &scene_params)
scene = new Scene(scene_params, device);
if (params.device == params.preferences_device) {
denoise_device = device;
}
else {
denoise_device = Device::create(params.preferences_device, stats, profiler);
if (denoise_device->have_error()) {
progress.set_error(denoise_device->error_message());
}
}
/* Configure path tracer. */
path_trace_ = make_unique<PathTrace>(
device, scene->film, &scene->dscene, render_scheduler_, tile_manager_);
device, denoise_device, scene->film, &scene->dscene, render_scheduler_, tile_manager_);
path_trace_->set_progress(&progress);
path_trace_->progress_update_cb = [&]() { update_status_time(); };
@ -91,6 +102,9 @@ Session::~Session()
/* Destroy scene and device. */
delete scene;
if (denoise_device != device) {
delete denoise_device;
}
delete device;
/* Stop task scheduler. */

View File

@ -35,7 +35,11 @@ class SceneParams;
class SessionParams {
public:
/* Device, which is choosen based on Blender Cycles preferences, as well as Scene settings and
* command line arguments. */
DeviceInfo device;
/* Device from Cycles preferences. */
DeviceInfo preferences_device;
bool headless;
bool background;
@ -104,6 +108,8 @@ class SessionParams {
class Session {
public:
Device *device;
/* Denoiser device. Could be the same as the path trace device. */
Device *denoise_device;
Scene *scene;
Progress progress;
SessionParams params;