/* SPDX-FileCopyrightText: 2011-2022 Blender Foundation * * SPDX-License-Identifier: Apache-2.0 */ #include #include #include "device/device.h" #include "scene/background.h" #include "scene/light.h" #include "scene/mesh.h" #include "scene/scene.h" #include "scene/shader.h" #include "scene/shader_graph.h" #include "scene/shader_nodes.h" #include "scene/stats.h" #include "scene/svm.h" #include "kernel/svm/node_types.h" #include "util/log.h" #include "util/map.h" #include "util/math_float3.h" #include "util/progress.h" #include "util/queue.h" #include "util/task.h" CCL_NAMESPACE_BEGIN /* Shader Manager */ SVMShaderManager::SVMShaderManager() = default; SVMShaderManager::~SVMShaderManager() = default; void SVMShaderManager::device_update_shader(Scene *scene, Shader *shader, Progress &progress, array *svm_nodes) { if (progress.get_cancel()) { return; } assert(shader->graph); SVMCompiler::Summary summary; SVMCompiler compiler(scene, progress); compiler.background = (shader == scene->background->get_shader(scene)); compiler.compile(shader, *svm_nodes, 0, &summary); LOG_DEBUG << "Compilation summary:\n" << "Shader name: " << shader->name << "\n" << summary.full_report(); } void SVMShaderManager::device_update_specific(Device *device, DeviceScene *dscene, Scene *scene, Progress &progress) { if (!need_update()) { return; } const scoped_callback_timer timer([scene](double time) { if (scene->update_stats) { scene->update_stats->svm.times.add_entry({"device_update", time}); } }); const int num_shaders = scene->shaders.size(); LOG_INFO << "Total " << num_shaders << " shaders."; const double start_time = time_dt(); /* test if we need to update */ device_free(device, dscene, scene); /* Build all shaders. */ TaskPool task_pool; vector> shader_svm_nodes(num_shaders); for (int i = 0; i < num_shaders; i++) { task_pool.push([this, scene, &progress, &shader_svm_nodes, i] { device_update_shader(scene, scene->shaders[i], progress, &shader_svm_nodes[i]); }); } task_pool.wait_work(); if (progress.get_cancel()) { return; } /* The global node list contains a jump table (one jump node per shader) * followed by the nodes of all shaders. */ const int jump_node_size = 1 + sizeof(SVMNodeShaderJump) / sizeof(int); int svm_nodes_size = num_shaders * jump_node_size; for (int i = 0; i < num_shaders; i++) { /* Since we're not copying the local jump node, the size ends up lower. */ svm_nodes_size += shader_svm_nodes[i].size() - jump_node_size; } int *svm_nodes = dscene->svm_nodes.alloc(svm_nodes_size); int node_offset = num_shaders * jump_node_size; for (int i = 0; i < num_shaders; i++) { Shader *shader = scene->shaders[i]; shader->clear_modified(); if (shader->emission_sampling != EMISSION_SAMPLING_NONE) { scene->light_manager->tag_update(scene, LightManager::SHADER_COMPILED); } /* Update the global jump table. * Each compiled shader starts with a jump node that has offsets local * to the shader, so copy those and add the offset into the global node list. */ const int base = shader->id * jump_node_size; svm_nodes[base + 0] = NODE_SHADER_JUMP; *reinterpret_cast(&svm_nodes[base + 1]) = SVMNodeShaderJump{ .offset_surface = shader_svm_nodes[i][1] - jump_node_size + node_offset, .offset_volume = shader_svm_nodes[i][2] - jump_node_size + node_offset, .offset_displacement = shader_svm_nodes[i][3] - jump_node_size + node_offset}; node_offset += shader_svm_nodes[i].size() - jump_node_size; } /* Copy the nodes of each shader into the correct location. */ int *dst = svm_nodes + num_shaders * jump_node_size; for (int i = 0; i < num_shaders; i++) { const int shader_size = shader_svm_nodes[i].size() - jump_node_size; std::copy_n(&shader_svm_nodes[i][jump_node_size], shader_size, dst); dst += shader_size; } if (progress.get_cancel()) { return; } device_update_common(device, dscene, scene, progress); update_flags = UPDATE_NONE; LOG_INFO << "Shader manager updated " << num_shaders << " shaders in " << time_dt() - start_time << " seconds."; } void SVMShaderManager::device_free(Device *device, DeviceScene *dscene, Scene *scene) { device_free_common(device, dscene, scene); dscene->svm_nodes.free(); } /* Graph Compiler */ SVMCompiler::SVMCompiler(Scene *scene, Progress &progress) : scene(scene), progress(progress) { max_stack_use = 0; current_type = SHADER_TYPE_SURFACE; current_shader = nullptr; current_graph = nullptr; background = false; mix_weight_offset = SVM_STACK_INVALID; bump_state_offset = SVM_STACK_INVALID; compile_failed = false; /* This struct has one entry for every node, in order of ShaderNodeType definition. */ svm_node_types_used = (std::atomic_int *)&scene->dscene.data.svm_usage; } int SVMCompiler::stack_size(SocketType::Type type) { int size = 0; switch (type) { case SocketType::FLOAT: case SocketType::INT: size = 1; break; case SocketType::COLOR: case SocketType::VECTOR: case SocketType::NORMAL: case SocketType::POINT: size = 3; break; case SocketType::CLOSURE: size = 0; break; default: assert(0); break; } return size; } int SVMCompiler::stack_size(const ShaderIO *io) { const SocketType::Type type = io->type(); const bool derivative = io->parent->need_derivatives(); return derivative ? stack_size(type) * 3 : stack_size(type); } SVMStackOffset SVMCompiler::stack_find_offset(const int size) { int offset = -1; /* find free space in stack & mark as used */ for (int i = 0, num_unused = 0; i < SVM_STACK_SIZE; i++) { if (active_stack.users[i]) { num_unused = 0; } else { num_unused++; } if (num_unused == size) { offset = i + 1 - size; max_stack_use = max(i + 1, max_stack_use); while (i >= offset) { active_stack.users[i--] = 1; } return offset; } } if (!compile_failed) { compile_failed = true; LOG_ERROR << "Shader graph: out of SVM stack space, shader \"" << current_shader->name << "\" too big."; } return 0; } SVMStackOffset SVMCompiler::stack_find_offset(const ShaderIO *io) { return stack_find_offset(stack_size(io)); } void SVMCompiler::stack_clear_offset(const ShaderIO *io, const SVMStackOffset offset) { const int size = stack_size(io); for (int i = 0; i < size; i++) { active_stack.users[offset + i]--; } } SVMStackOffset SVMCompiler::stack_assign(ShaderInput *input) { /* stack offset assign? */ if (input->stack_offset == SVM_STACK_INVALID) { if (input->link) { /* linked to output -> use output offset */ assert(input->link->stack_offset != SVM_STACK_INVALID); input->stack_offset = input->link->stack_offset; } else { ShaderNode *node = input->parent; /* not linked to output -> add nodes to load default value */ input->stack_offset = stack_find_offset(input); if (input->type() == SocketType::FLOAT) { add_value_node(node, node->get_float(input->socket_type), input->stack_offset); } else if (input->type() == SocketType::INT) { add_value_node( node, __int_as_float(node->get_int(input->socket_type)), input->stack_offset); } else if (input->type() == SocketType::VECTOR || input->type() == SocketType::NORMAL || input->type() == SocketType::POINT || input->type() == SocketType::COLOR) { add_value_node(node, node->get_float3(input->socket_type), input->stack_offset); } else { /* should not get called for closure */ assert(0); } } } return input->stack_offset; } SVMStackOffset SVMCompiler::stack_assign(ShaderOutput *output) { /* if no stack offset assigned yet, find one */ if (output->stack_offset == SVM_STACK_INVALID) { output->stack_offset = stack_find_offset(output); } return output->stack_offset; } SVMInputInt SVMCompiler::input_int(const char *name) { ShaderInput *input = current_node->input(name); SVMInputInt result = {input->parent->get_int(input->socket_type), SVM_STACK_INVALID}; if (input->link) { result.offset = stack_assign(input); } return result; } SVMInputFloat SVMCompiler::input_float(const char *name) { ShaderInput *input = current_node->input(name); if (input->link) { return SVMInputFloat{SVM_INPUT_STACK_OFFSET_MASK | uint(stack_assign(input))}; } float default_value = input->parent->get_float(input->socket_type); /* Filter out NaN that would collide with SVM_INPUT_STACK_OFFSET_MASK. */ if (!isfinite_safe(default_value)) { default_value = 0.0f; } return SVMInputFloat{__float_as_uint(default_value)}; } SVMInputFloat3 SVMCompiler::input_float3(const char *name) { ShaderInput *input = current_node->input(name); if (input->link) { return SVMInputFloat3{ SVMInputFloat{SVM_INPUT_STACK_OFFSET_MASK | uint(stack_assign(input))}, {0}, {0}}; } float3 default_value = input->parent->get_float3(input->socket_type); /* Filter out NaN that would collide with SVM_INPUT_STACK_OFFSET_MASK. */ if (!isfinite_safe(default_value)) { default_value = zero_float3(); } return SVMInputFloat3{ {__float_as_uint(default_value.x)}, {__float_as_uint(default_value.y)}, {__float_as_uint(default_value.z)}, }; } SVMInputFloat3 SVMCompiler::input_float3_from_offset(const SVMStackOffset offset) { return SVMInputFloat3{SVMInputFloat{SVM_INPUT_STACK_OFFSET_MASK | uint(offset)}, {0}, {0}}; } SVMStackOffset SVMCompiler::input_link(const char *name) { /* This is for sockets like normal which always expect a link. For the constant_folded_in we have * to write the value to the stack with another load and return a linked svm offset, as these * never store the default value in the SVMNode. */ ShaderInput *input = current_node->input(name); /* Ensure input link is pushed to SVM before the node itself. */ assert(!(current_node->added_to_svm && input->constant_folded_in && input->link == nullptr && input->stack_offset == SVM_STACK_INVALID)); return (input->link || input->constant_folded_in) ? stack_assign(input) : SVM_STACK_INVALID; } SVMStackOffset SVMCompiler::output(const char *name) { ShaderOutput *shader_output = current_node->output(name); return output(shader_output); } SVMStackOffset SVMCompiler::output(ShaderOutput *shader_output) { return (!shader_output->links.empty()) ? stack_assign(shader_output) : SVM_STACK_INVALID; } void SVMCompiler::stack_link(ShaderInput *input, ShaderOutput *output) { if (output->stack_offset == SVM_STACK_INVALID) { assert(input->link); assert(stack_size(output->type()) == stack_size(input->link->type())); output->stack_offset = input->link->stack_offset; const int size = stack_size(output); for (int i = 0; i < size; i++) { active_stack.users[output->stack_offset + i]++; } } } bool SVMCompiler::is_sole_user(const ShaderNode *node, const ShaderOutput *output, const ShaderNodeSet &done) { /* Check if the node is the only remaining user of the output, meaning the * output's stack space can be freed once the node is compiled. */ /* optimization we should add: verify if in->parent is actually used */ for (const ShaderInput *in : output->links) { if (in->parent != node && !done.contains(in->parent)) { return false; } } return true; } void SVMCompiler::stack_clear_users(ShaderNode *node, ShaderNodeSet &done) { /* Possible minor optimization: If all nodes read all inputs before writing outputs, * the input stack space could be reused for the output and cache locality would be * improved. This was tried at some point but disabled, it would need careful validation * of stack assignment code and every SVM node implementation. It's not obvious if it's * worth it. */ for (ShaderInput *input : node->inputs) { ShaderOutput *output = input->link; if (output && output->stack_offset != SVM_STACK_INVALID && is_sole_user(node, output, done)) { stack_clear_offset(output, output->stack_offset); output->stack_offset = SVM_STACK_INVALID; for (ShaderInput *in : output->links) { in->stack_offset = SVM_STACK_INVALID; } } } } void SVMCompiler::stack_clear_temporary(ShaderNode *node) { for (ShaderInput *input : node->inputs) { if (!input->link && input->stack_offset != SVM_STACK_INVALID) { stack_clear_offset(input, input->stack_offset); input->stack_offset = SVM_STACK_INVALID; } } } void SVMCompiler::add_node(ShaderNodeType type) { svm_node_types_used[type] = true; current_svm_nodes.push_back_slow(type); } static ShaderNodeType svm_node_type_with_derivatives(ShaderNodeType type) { switch (type) { #define SHADER_NODE_TYPE_DERIVATIVE(name) \ case name: \ return name##_DERIVATIVE; #include "kernel/svm/node_types_template.h" default: break; } return type; } ShaderNodeType SVMCompiler::node_type(const ShaderNode *shader_node, const ShaderNodeType type, const bool use_derivatives) { if ((use_derivatives || (shader_node && shader_node->need_derivatives())) && current_type != SHADER_TYPE_VOLUME) { return svm_node_type_with_derivatives(type); } return type; } void SVMCompiler::add_node_data_float4(const float4 &f) { current_svm_nodes.push_back_slow(__float_as_int(f.x)); current_svm_nodes.push_back_slow(__float_as_int(f.y)); current_svm_nodes.push_back_slow(__float_as_int(f.z)); current_svm_nodes.push_back_slow(__float_as_int(f.w)); } void SVMCompiler::add_node_data_float(const float f) { current_svm_nodes.push_back_slow(__float_as_int(f)); } void SVMCompiler::add_value_node(ShaderNode *shader_node, const float value, const int stack_offset) { add_node(shader_node, NODE_VALUE_F, SVMNodeValueF{ .value = value, .out_offset = (SVMStackOffset)stack_offset, }); } void SVMCompiler::add_value_node(ShaderNode *shader_node, const float3 &value, const int stack_offset) { add_node(shader_node, NODE_VALUE_V, SVMNodeValueV{ .out_offset = (SVMStackOffset)stack_offset, .value = value, }); } void SVMCompiler::stack_zero_incomplete_derivatives(const ShaderNode *node) { /* No derivatives in volumes yet. */ if (current_type == SHADER_TYPE_VOLUME) { return; } /* Does this node need derivatives but it doesn't have a derivative variation? */ const bool incomplete_derivatives = node->need_derivatives() && svm_node_type_with_derivatives(node->shader_node_type()) == node->shader_node_type(); if (!incomplete_derivatives) { return; } /* Zero derivatives. Note we can not use add_value_node since it will * automatically write derivatives. */ for (const ShaderOutput *output : node->outputs) { if (output->stack_offset == SVM_STACK_INVALID) { continue; } const int base_size = stack_size(output->type()); if (base_size == 3) { add_value_node(nullptr, zero_float3(), output->stack_offset + 3); add_value_node(nullptr, zero_float3(), output->stack_offset + 6); } else if (base_size == 1) { add_value_node(nullptr, 0.0f, output->stack_offset + 1); add_value_node(nullptr, 0.0f, output->stack_offset + 2); } } } uint SVMCompiler::attribute(ustring name) { return scene->shader_manager->get_attribute_id(name); } uint SVMCompiler::attribute(AttributeStandard std) { return scene->shader_manager->get_attribute_id(std); } uint SVMCompiler::attribute_standard(ustring name) { const AttributeStandard std = Attribute::name_standard(name.c_str()); return (std) ? attribute(std) : attribute(name); } void SVMCompiler::find_dependencies(ShaderNodeSet &dependencies, const ShaderNodeSet &done, ShaderInput *input, ShaderNode *skip_node) { ShaderNode *node = (input->link) ? input->link->parent : nullptr; if (node != nullptr && !done.contains(node) && node != skip_node && !dependencies.contains(node)) { for (ShaderInput *in : node->inputs) { find_dependencies(dependencies, done, in, skip_node); } dependencies.insert(node); } } void SVMCompiler::generate_node(ShaderNode *node, ShaderNodeSet &done) { current_node = node; node->compile(*this); current_node = nullptr; stack_zero_incomplete_derivatives(node); stack_clear_users(node, done); stack_clear_temporary(node); if (current_type == SHADER_TYPE_SURFACE) { if (node->has_spatial_varying()) { current_shader->has_surface_spatial_varying = true; } if (node->get_feature() & KERNEL_FEATURE_NODE_RAYTRACE) { current_shader->has_surface_raytrace = true; } } else if (current_type == SHADER_TYPE_VOLUME) { if (node->has_spatial_varying()) { current_shader->has_volume_spatial_varying = true; } if (node->has_attribute_dependency()) { current_shader->has_volume_attribute_dependency = true; } } } int SVMCompiler::stack_node_output_size(const ShaderNode *node) { /* Compute stack size that will be allocated by this node. */ int size = 0; for (const ShaderOutput *output : node->outputs) { if (!output->links.empty() && output->stack_offset == SVM_STACK_INVALID) { size += stack_size(output); } } return size; } void SVMCompiler::generate_svm_nodes(const ShaderNodeSet &nodes, CompilerState *state) { ShaderNodeSet &done = state->nodes_done; vector &done_flag = state->nodes_done_flag; /* Schedule the nodes to reduce peak SVM stack usage with a Sethi-Ullman style * heuristic. This is optimal for trees, but only a heuristic for DAGs where * it's an NP-hard problem. * * Nodes whose sub-graphs need the most stack are generated first, which helps * complete tightly related sub-graphs before handling other parts of the graph. * * See "Generalizations of the Sethi-Ullman algorithm for register allocation" * by Appel and Supowit for details. We use different terminology as some of it * conflicts with our own. * * The Sethi-Ullman number is the peak SVM stack size needed to evaluate a node * and "producer" nodes feeding into it, including the node output stack size. * * It was proven that evaluating producer nodes by descending order of this number * minus the output stack size is optimal for trees. For a graph, we approximate * this by only counting the output stack size for a producer node with multiple * consumers. */ /* Producer nodes feeding into #node that have not been scheduled yet. */ auto get_producers = [&](const ShaderNode *node, vector &producers) { producers.clear(); for (const ShaderInput *input : node->inputs) { if (input->link) { ShaderNode *producer = input->link->parent; if (!done_flag[producer->id] && std::find(producers.begin(), producers.end(), producer) == producers.end()) { producers.push_back(producer); } } } }; /* Number of unique unscheduled consumer nodes for each producer node. */ unordered_map num_consumers; vector consumers; for (ShaderNode *node : nodes) { if (done_flag[node->id]) { continue; } consumers.clear(); for (const ShaderOutput *output : node->outputs) { for (const ShaderInput *in : output->links) { ShaderNode *consumer = in->parent; if (!done_flag[consumer->id] && nodes.contains(consumer) && std::find(consumers.begin(), consumers.end(), consumer) == consumers.end()) { consumers.push_back(consumer); } } } num_consumers[node] = consumers.size(); } /* Sethi-Ullman number for each node. */ unordered_map sethi_ullman_number; /* Current Sethi-Ullman number for graph scheduling, only counting the output * size when there are multiple consumers. */ auto current_sethi_ullman_number = [&](ShaderNode *node) -> int { return (num_consumers[node] > 1) ? stack_node_output_size(node) : sethi_ullman_number[node]; }; /* Order producers by Sethi-Ullman number. Node ID is the tie breaker. */ auto node_order_key = [&](ShaderNode *node) -> int { return current_sethi_ullman_number(node) - stack_node_output_size(node); }; auto node_order_compare = [&](ShaderNode *a, ShaderNode *b) { return node_order_key(a) > node_order_key(b) || (node_order_key(a) == node_order_key(b) && a->id < b->id); }; /* Compute Sethi-Ullman number recursively. */ std::function compute_sethi_ullman_number = [&](ShaderNode *node) -> int { const auto it = sethi_ullman_number.find(node); if (it != sethi_ullman_number.end()) { return it->second; } vector producers; get_producers(node, producers); for (ShaderNode *producer : producers) { compute_sethi_ullman_number(producer); } std::sort(producers.begin(), producers.end(), node_order_compare); /* Sum output and peak stack usage of producers. */ int output_size = 0; int peak_size = 0; for (ShaderNode *producer : producers) { peak_size = max(peak_size, output_size + current_sethi_ullman_number(producer)); output_size += stack_node_output_size(producer); } peak_size = max(peak_size, output_size + stack_node_output_size(node)); sethi_ullman_number[node] = peak_size; return peak_size; }; /* Gather all sink nodes (that have no unscheduled consumers) and sort by * Sethi-Ullman number. */ vector sinks; for (ShaderNode *node : nodes) { if (done_flag[node->id]) { continue; } compute_sethi_ullman_number(node); bool is_sink = true; for (const ShaderOutput *output : node->outputs) { for (const ShaderInput *in : output->links) { if (!done_flag[in->parent->id] && nodes.contains(in->parent)) { is_sink = false; break; } } if (!is_sink) { break; } } if (is_sink) { sinks.push_back(node); } } std::sort(sinks.begin(), sinks.end(), node_order_compare); /* Generate nodes recursively from sink nodes. */ std::function generate = [&](ShaderNode *node) { if (done_flag[node->id]) { return; } vector producers; get_producers(node, producers); std::sort(producers.begin(), producers.end(), node_order_compare); for (ShaderNode *producer : producers) { generate(producer); } generate_node(node, done); done.insert(node); done_flag[node->id] = true; }; for (ShaderNode *node : sinks) { generate(node); } } void SVMCompiler::generate_closure_node(ShaderNode *node, CompilerState *state) { /* Skip generating closure that are not supported or needed for a particular * type of shader. For example a BSDF in a volume shader. */ const uint node_feature = node->get_feature(); if ((state->node_feature_mask & node_feature) != node_feature) { return; } /* execute dependencies for closure */ for (ShaderInput *in : node->inputs) { if (in->link != nullptr) { ShaderNodeSet dependencies; find_dependencies(dependencies, state->nodes_done, in); generate_svm_nodes(dependencies, state); } } /* closure mix weight */ const char *weight_name = (current_type == SHADER_TYPE_VOLUME) ? "VolumeMixWeight" : "SurfaceMixWeight"; ShaderInput *weight_in = node->input(weight_name); if (weight_in && (weight_in->link || node->get_float(weight_in->socket_type) != 1.0f)) { mix_weight_offset = stack_assign(weight_in); } else { mix_weight_offset = SVM_STACK_INVALID; } /* compile closure itself */ generate_node(node, state->nodes_done); mix_weight_offset = SVM_STACK_INVALID; if (current_type == SHADER_TYPE_SURFACE) { if (node->has_surface_transparent()) { current_shader->has_surface_transparent = true; } if (node->has_surface_bssrdf()) { current_shader->has_surface_bssrdf = true; if (node->has_bssrdf_bump()) { current_shader->has_bssrdf_bump = true; } } if (node->has_bump()) { current_shader->has_bump_from_surface = true; } } } void SVMCompiler::generated_shared_closure_nodes(ShaderNode *root_node, ShaderNode *node, CompilerState *state, const ShaderNodeSet &shared) { if (shared.contains(node)) { generate_multi_closure(root_node, node, state); } else { for (ShaderInput *in : node->inputs) { if (in->type() == SocketType::CLOSURE && in->link) { generated_shared_closure_nodes(root_node, in->link->parent, state, shared); } } } } void SVMCompiler::find_aov_nodes_and_dependencies(ShaderNodeSet &aov_nodes, ShaderGraph *graph, CompilerState *state) { for (ShaderNode *node : graph->nodes) { if (node->special_type == SHADER_SPECIAL_TYPE_OUTPUT_AOV) { OutputAOVNode *aov_node = static_cast(node); if (aov_node->offset >= 0) { aov_nodes.insert(aov_node); for (ShaderInput *in : node->inputs) { if (in->link != nullptr) { find_dependencies(aov_nodes, state->nodes_done, in); } } } } } } void SVMCompiler::generate_multi_closure(ShaderNode *root_node, ShaderNode *node, CompilerState *state) { /* only generate once */ if (state->closure_done.contains(node)) { return; } state->closure_done.insert(node); if (node->special_type == SHADER_SPECIAL_TYPE_COMBINE_CLOSURE) { /* weighting is already taken care of in ShaderGraph::transform_multi_closure */ ShaderInput *cl1in = node->input("Closure1"); ShaderInput *cl2in = node->input("Closure2"); ShaderInput *facin = node->input("Fac"); /* skip empty mix/add closure nodes */ if (!cl1in->link && !cl2in->link) { return; } if (facin && facin->link) { /* mix closure: generate instructions to compute mix weight */ ShaderNodeSet dependencies; find_dependencies(dependencies, state->nodes_done, facin); generate_svm_nodes(dependencies, state); /* execute shared dependencies. this is needed to allow skipping * of zero weight closures and their dependencies later, so we * ensure that they only skip dependencies that are unique to them */ ShaderNodeSet cl1deps; ShaderNodeSet cl2deps; ShaderNodeSet shareddeps; find_dependencies(cl1deps, state->nodes_done, cl1in); find_dependencies(cl2deps, state->nodes_done, cl2in); const ShaderNodeIDComparator node_id_comp; set_intersection(cl1deps.begin(), cl1deps.end(), cl2deps.begin(), cl2deps.end(), std::inserter(shareddeps, shareddeps.begin()), node_id_comp); /* it's possible some nodes are not shared between this mix node * inputs, but still needed to be always executed, this mainly * happens when a node of current subbranch is used by a parent * node or so */ if (root_node != node) { for (ShaderInput *in : root_node->inputs) { ShaderNodeSet rootdeps; find_dependencies(rootdeps, state->nodes_done, in, node); set_intersection(rootdeps.begin(), rootdeps.end(), cl1deps.begin(), cl1deps.end(), std::inserter(shareddeps, shareddeps.begin()), node_id_comp); set_intersection(rootdeps.begin(), rootdeps.end(), cl2deps.begin(), cl2deps.end(), std::inserter(shareddeps, shareddeps.begin()), node_id_comp); } } /* For dependencies AOV nodes, prevent them from being categorized * as exclusive deps of one or the other closure, since the need to * execute them for AOV writing is not dependent on the closure * weights. */ if (!state->aov_nodes.empty()) { set_intersection(state->aov_nodes.begin(), state->aov_nodes.end(), cl1deps.begin(), cl1deps.end(), std::inserter(shareddeps, shareddeps.begin()), node_id_comp); set_intersection(state->aov_nodes.begin(), state->aov_nodes.end(), cl2deps.begin(), cl2deps.end(), std::inserter(shareddeps, shareddeps.begin()), node_id_comp); } if (!shareddeps.empty()) { if (cl1in->link) { generated_shared_closure_nodes(root_node, cl1in->link->parent, state, shareddeps); } if (cl2in->link) { generated_shared_closure_nodes(root_node, cl2in->link->parent, state, shareddeps); } generate_svm_nodes(shareddeps, state); } /* generate instructions for input closure 1 */ if (cl1in->link) { /* Add instruction to skip closure and its dependencies if mix * weight is zero. */ const int node_start = current_svm_nodes.size(); add_node(nullptr, NODE_JUMP_IF_ONE, SVMNodeJumpIfOne{0, stack_assign(facin)}); generate_multi_closure(root_node, cl1in->link->parent, state); /* Fill in jump instruction location to be after closure. */ const int jump_node_size = 1 + sizeof(SVMNodeJumpIfOne) / sizeof(int); current_svm_nodes[node_start + 1] = current_svm_nodes.size() - (node_start + jump_node_size); } /* generate instructions for input closure 2 */ if (cl2in->link) { /* Add instruction to skip closure and its dependencies if mix * weight is zero. */ const int node_start = current_svm_nodes.size(); add_node(nullptr, NODE_JUMP_IF_ZERO, SVMNodeJumpIfZero{0, stack_assign(facin)}); generate_multi_closure(root_node, cl2in->link->parent, state); /* Fill in jump instruction location to be after closure. */ const int jump_node_size = 1 + sizeof(SVMNodeJumpIfZero) / sizeof(int); current_svm_nodes[node_start + 1] = current_svm_nodes.size() - (node_start + jump_node_size); } /* unassign */ facin->stack_offset = SVM_STACK_INVALID; } else { /* execute closures and their dependencies, no runtime checks * to skip closures here because was already optimized due to * fixed weight or add closure that always needs both */ if (cl1in->link) { generate_multi_closure(root_node, cl1in->link->parent, state); } if (cl2in->link) { generate_multi_closure(root_node, cl2in->link->parent, state); } } } else { generate_closure_node(node, state); } state->nodes_done.insert(node); state->nodes_done_flag[node->id] = true; } static void mark_nodes_requiring_derivatives(const SVMCompiler &compiler, ShaderGraph *graph, const ShaderType type) { if (type == SHADER_TYPE_VOLUME) { /* Only support derivatives for surface for now. */ return; } queue traverse_queue; ShaderNodeSet scheduled; /* Check if texture nodes need derivatives. */ for (ShaderNode *node : graph->nodes) { if (node->is_texture_node_and_needs_derivatives(compiler)) { traverse_queue.push(node); scheduled.insert(node); } } /* Mark all ancestors of texture nodes as requiring derivatives, if the texture nodes themselves * need derivatives. */ while (!traverse_queue.empty()) { ShaderNode *node = traverse_queue.front(); traverse_queue.pop(); node->set_need_derivatives(); LOG_DEBUG << "Marking " << node->name << " as requiring derivatives"; for (ShaderInput *input : node->inputs) { if (input->link == nullptr) { continue; } if (scheduled.contains(input->link->parent)) { continue; } traverse_queue.push(input->link->parent); scheduled.insert(input->link->parent); } } } void SVMCompiler::compile_type(Shader *shader, ShaderGraph *graph, ShaderType type) { /* Converting a shader graph into svm_nodes that can be executed * sequentially on the virtual machine is fairly simple. We can keep * looping over nodes and each time all the inputs of a node are * ready, we add svm_nodes for it that read the inputs from the * stack and write outputs back to the stack. * * With the SVM, we always sample only a single closure. We can think * of all closures nodes as a binary tree with mix closures as inner * nodes and other closures as leafs. The SVM will traverse that tree, * each time deciding to go left or right depending on the mix weights, * until a closure is found. * * We only execute nodes that are needed for the mix weights and chosen * closure. */ current_type = type; current_graph = graph; /* get input in output node */ ShaderNode *output = graph->output(); ShaderInput *clin = nullptr; switch (type) { case SHADER_TYPE_SURFACE: clin = output->input("Surface"); break; case SHADER_TYPE_VOLUME: clin = output->input("Volume"); break; case SHADER_TYPE_DISPLACEMENT: clin = output->input("Displacement"); break; case SHADER_TYPE_BUMP: clin = output->input("Normal"); break; default: assert(0); break; } /* clear all compiler state */ memset((void *)&active_stack, 0, sizeof(active_stack)); current_svm_nodes.clear(); for (ShaderNode *node : graph->nodes) { node->added_to_svm = false; for (ShaderInput *input : node->inputs) { input->stack_offset = SVM_STACK_INVALID; } for (ShaderOutput *output : node->outputs) { output->stack_offset = SVM_STACK_INVALID; } } mark_nodes_requiring_derivatives(*this, graph, type); /* for the bump shader we need add a node to store the shader state */ const bool need_bump_state = (type == SHADER_TYPE_BUMP) && (shader->get_displacement_method() == DISPLACE_BOTH); if (need_bump_state) { bump_state_offset = stack_find_offset(SVM_BUMP_EVAL_STATE_SIZE); add_node( nullptr, NODE_ENTER_BUMP_EVAL, SVMNodeEnterBumpEval{.state_offset = bump_state_offset}); } if (shader->reference_count()) { CompilerState state(graph); switch (type) { case SHADER_TYPE_SURFACE: /* generate surface shader */ find_aov_nodes_and_dependencies(state.aov_nodes, graph, &state); if (shader->has_surface) { state.node_feature_mask = KERNEL_FEATURE_NODE_MASK_SURFACE; } break; case SHADER_TYPE_VOLUME: /* generate volume shader */ if (shader->has_volume) { state.node_feature_mask = KERNEL_FEATURE_NODE_MASK_VOLUME; } break; case SHADER_TYPE_DISPLACEMENT: /* generate displacement shader */ if (shader->has_displacement) { state.node_feature_mask = KERNEL_FEATURE_NODE_MASK_DISPLACEMENT; } break; case SHADER_TYPE_BUMP: /* generate bump shader */ if (clin->link) { state.node_feature_mask = KERNEL_FEATURE_NODE_MASK_BUMP; } break; default: break; } if (clin->link) { generate_multi_closure(clin->link->parent, clin->link->parent, &state); } /* compile output node */ current_node = output; output->compile(*this); current_node = nullptr; if (!state.aov_nodes.empty()) { /* AOV passes are only written if the object is directly visible, so * there is no point in evaluating all the nodes generated only for the * AOV outputs if that's not the case. Therefore, we insert * NODE_AOV_START into the shader before the AOV-only nodes are * generated which tells the kernel that it can stop evaluation * early if AOVs will not be written. */ add_node(NODE_AOV_START); generate_svm_nodes(state.aov_nodes, &state); } } /* add node to restore state after bump shader has finished */ if (need_bump_state) { add_node( nullptr, NODE_LEAVE_BUMP_EVAL, SVMNodeLeaveBumpEval{.state_offset = bump_state_offset}); bump_state_offset = SVM_STACK_INVALID; } /* if compile failed, generate empty shader */ if (compile_failed) { current_svm_nodes.clear(); compile_failed = false; } /* for bump shaders we fall thru to the surface shader, but if this is any other kind of shader * it ends here */ if (type != SHADER_TYPE_BUMP) { add_node(NODE_END); } } void SVMCompiler::compile(Shader *shader, array &svm_nodes, const int index, Summary *summary) { svm_node_types_used[NODE_SHADER_JUMP] = true; add_node(nullptr, NODE_SHADER_JUMP, SVMNodeShaderJump{0, 0, 0}); svm_nodes.append(current_svm_nodes); current_svm_nodes.clear(); /* copy graph for shader with bump mapping */ const int start_num_svm_nodes = svm_nodes.size(); const double time_start = time_dt(); const bool has_bump_from_displacement = shader->has_bump_from_displacement; current_shader = shader; /* generate bump shader */ if (has_bump_from_displacement) { const scoped_timer timer((summary != nullptr) ? &summary->time_generate_bump : nullptr); compile_type(shader, shader->graph.get(), SHADER_TYPE_BUMP); svm_nodes[index + 1] = svm_nodes.size(); svm_nodes.append(current_svm_nodes); } /* generate surface shader */ { const scoped_timer timer((summary != nullptr) ? &summary->time_generate_surface : nullptr); compile_type(shader, shader->graph.get(), SHADER_TYPE_SURFACE); /* only set jump offset if there's no bump shader, as the bump shader will fall thru to this * one if it exists */ if (!has_bump_from_displacement) { svm_nodes[index + 1] = svm_nodes.size(); } svm_nodes.append(current_svm_nodes); } /* generate volume shader */ { const scoped_timer timer((summary != nullptr) ? &summary->time_generate_volume : nullptr); compile_type(shader, shader->graph.get(), SHADER_TYPE_VOLUME); svm_nodes[index + 2] = svm_nodes.size(); svm_nodes.append(current_svm_nodes); } /* generate displacement shader */ { const scoped_timer timer((summary != nullptr) ? &summary->time_generate_displacement : nullptr); compile_type(shader, shader->graph.get(), SHADER_TYPE_DISPLACEMENT); svm_nodes[index + 3] = svm_nodes.size(); svm_nodes.append(current_svm_nodes); } /* Fill in summary information. */ if (summary != nullptr) { summary->time_total = time_dt() - time_start; summary->peak_stack_usage = max_stack_use; summary->num_svm_nodes = svm_nodes.size() - start_num_svm_nodes; } /* Estimate emission for MIS. */ shader->estimate_emission(); } /* Compiler summary implementation. */ SVMCompiler::Summary::Summary() : num_svm_nodes(0), peak_stack_usage(0), time_generate_surface(0.0), time_generate_bump(0.0), time_generate_volume(0.0), time_generate_displacement(0.0), time_total(0.0) { } string SVMCompiler::Summary::full_report() const { string report; report += string_printf("Number of SVM nodes: %d\n", num_svm_nodes); report += string_printf("Peak stack usage: %d\n", peak_stack_usage); report += string_printf("Time (in seconds):\n"); report += string_printf("Generate: %f\n", time_generate_surface + time_generate_bump + time_generate_volume + time_generate_displacement); report += string_printf(" Surface: %f\n", time_generate_surface); report += string_printf(" Bump: %f\n", time_generate_bump); report += string_printf(" Volume: %f\n", time_generate_volume); report += string_printf(" Displacement: %f\n", time_generate_displacement); return report; } /* Global state of the compiler. */ SVMCompiler::CompilerState::CompilerState(ShaderGraph *graph) { int max_id = 0; for (ShaderNode *node : graph->nodes) { max_id = max(node->id, max_id); } nodes_done_flag.resize(max_id + 1, false); node_feature_mask = 0; } CCL_NAMESPACE_END