SVM是cycles中使用的shader执行架构,全称为 Shader Virtual Machine,即Shader虚拟机。因为cycles是支持shader graph的,需要有一种方式将各个shader节点及它们的连接方式解析出来,并按照正确的顺序执行,SVM就是为了实现这个功能。
在详细分析SVM之前,先了解cycles的Shader系统中的各个概念。

ShaderNode

ShaderNode是ShaderGraph中的一个Node节点,比如下面的这个Blender中最常用的 BSDF节点,

一个node节点包含了多个输入和输出槽,可以连接到其他节点。
ShaderNode类是cycles中所有节点的基类,它继承自Node类(cycles中所有场景中的物体都是Node),包含了每一个node节点都有的属性和方法。

成员变量

ShaderNode类有5个成员:

class ShaderNode : public Node {
  ...  
  vector<ShaderInput *> inputs;
  vector<ShaderOutput *> outputs;

  int id;          /* index in graph node array */
  ShaderBump bump; /* for bump mapping utility */

  ShaderNodeSpecialType special_type; /* special node type */
  ...
}

inputs和outputs很好理解,表示该节点的输入输出槽,它们的类型ShaderInput和ShaderOutput后面介绍。
id是该node在ShaderGraph中的位置索引。
bump用于bump mapping(凹凸贴图),bump mapping需要重建法线,可能会影响node的计算,ShaderBump定义如下:

/* Bump
 *
 * For bump mapping, a node may be evaluated multiple times, using different
 * samples to reconstruct the normal, this indicates the sample position */

enum ShaderBump { SHADER_BUMP_NONE, SHADER_BUMP_CENTER, SHADER_BUMP_DX, SHADER_BUMP_DY };

表示重建法线时多次采样的位置。
special_type表示该node的特殊类型,只有某些特殊的节点才会用到这个属性,ShaderNodeSpecialType定义:

enum ShaderNodeSpecialType {
  SHADER_SPECIAL_TYPE_NONE,
  SHADER_SPECIAL_TYPE_PROXY,
  SHADER_SPECIAL_TYPE_AUTOCONVERT,
  SHADER_SPECIAL_TYPE_GEOMETRY,
  SHADER_SPECIAL_TYPE_OSL,
  SHADER_SPECIAL_TYPE_IMAGE_SLOT,
  SHADER_SPECIAL_TYPE_CLOSURE,
  SHADER_SPECIAL_TYPE_COMBINE_CLOSURE,
  SHADER_SPECIAL_TYPE_OUTPUT,
  SHADER_SPECIAL_TYPE_BUMP,
  SHADER_SPECIAL_TYPE_OUTPUT_AOV,
};

构造函数

ShaderNode构造函数如下:

ShaderNode::ShaderNode(const NodeType *type) : Node(type)
{
  name = type->name;
  id = -1;
  bump = SHADER_BUMP_NONE;
  special_type = SHADER_SPECIAL_TYPE_NONE;

  create_inputs_outputs(type);
}

它接收一个NodeType类型的参数,将name初始化为NodeType的name(name成员是从Node类里继承来的),并将id, bump, special_type初始化成默认值,最后调用create_inputs_outputs(type),根具NodeType初始化输出输出槽。
关于NodeType,只需要知道它有一个name,一个type(NONE或者SHADER),以及输出和输出槽的信息。
create_inputs_outputs定义如下:

void ShaderNode::create_inputs_outputs(const NodeType *type)
{
  foreach (const SocketType &socket, type->inputs) {
    if (socket.flags & SocketType::LINKABLE) {
      inputs.push_back(new ShaderInput(socket, this));
    }
  }

  foreach (const SocketType &socket, type->outputs) {
    outputs.push_back(new ShaderOutput(socket, this));
  }
}

根据NodeType中的信息初始化inputs和outputs两个属性。

一些重要的成员函数

virtual ShaderNode *clone(ShaderGraph *graph) const = 0;
从ShaderGraph中创建一个当前ShaderNode的拷贝,由具体的节点实现。
virtual void compile(SVMCompiler &compiler) = 0;
定义该节点如何被SVM编译,由具体的节点实现。

  /* Get closure ID to which the node compiles into. */
  virtual ClosureType get_closure_type()
  {
    return CLOSURE_NONE_ID;
  }

返回该节点的closure ID,这将决定该节点会被编译到哪个closure中,关于closure后面会详细介绍。

ShaderInput

ShaderInput表示ShaderNode的一个输入槽,这个类的定义比较简单,它有以下成员变量:

class ShaderInput{
  ...
  const SocketType &socket_type;
  ShaderNode *parent;
  ShaderOutput *link;
  int stack_offset; /* for SVM compiler */
  ...
}

socket_type表示该输入槽的数值类型,比如INT,FLOAT,COLOR,VECTOR,也包含了该输入槽的名称等信息。
parent指向该输入槽所属的节点,在ShaderNode中初始化ShaderInput是可以看到传入和this指针。
link指向与该输入槽连接的输出槽,一个输入槽只能与一个输出槽连接。
stack_offset用于SVM编译,后续介绍。

ShaderOutput

ShaderOutput表示ShaderNode的一个输出槽,与ShaderInput相似,它有以下成员变量:

class ShaderInput{
  ...  
  const SocketType &socket_type;
  ShaderNode *parent;
  vector<ShaderInput *> links;
  int stack_offset; /* for SVM compiler */
  ...
}

socket_type, parent, stack_offset三个成员的含义ShaderInput相同。
links指向此输出槽连接的所有输入槽,一个输出槽可以连接多个输入槽,所以它是一个vector。

ShaderGraph

Shader Graph包含了多个Shader Node以及它们的连接关系,并且可以增加或者删除节点及它们的连线。

关于Closure

在详细了解Shader Graph之前,需要先了解Shader系统中Closure的概念。
Closure中文翻译为闭包,在计算机学科中,闭包 = 函数 + 引用环境,即要执行的程序代码加上它引用的各个变量。在Cycles的Shader系统中,如果将每个Shader Node都视为一个函数的话,闭包就等于多个节点连接形成的计算逻辑及它们使用的参数的集合。然而,并不是在Shader Graph随意挑选几个连接好的节点就可以将它们视作一个闭包,在Shader系统中,只用Shader节点及它的前置输入节点的集合才能视作一个闭包,比如下面这个Shader Graph就有一个简单的闭包:

这个闭包包含了Glass BSDF这个Shader节点和它的前置输入节点Value。
这里所说的Shader节点并不是指Shader Node,而是指Shader Node中执行了较复杂的Shader计算的节点,Cycles将这种节点归类到Shader这个分类下面:

在上面的ShaderNode类中有一个get_closure_type函数,它返回ClosureType类型的枚举,并不是所有Shader Node都返回有效的ClosureType,只有上面的Shader才有ClosureType,ClosureType的定义如下:

typedef enum ClosureType {
  /* Special type, flags generic node as a non-BSDF. */
  CLOSURE_NONE_ID,

  CLOSURE_BSDF_ID,

  /* Diffuse */
  CLOSURE_BSDF_DIFFUSE_ID,
  CLOSURE_BSDF_OREN_NAYAR_ID,
  CLOSURE_BSDF_DIFFUSE_RAMP_ID,
  CLOSURE_BSDF_SHEEN_ID,
  CLOSURE_BSDF_DIFFUSE_TOON_ID,
  CLOSURE_BSDF_TRANSLUCENT_ID,

  /* Glossy */
  CLOSURE_BSDF_MICROFACET_GGX_ID,
  CLOSURE_BSDF_MICROFACET_BECKMANN_ID,
  CLOSURE_BSDF_MICROFACET_MULTI_GGX_ID, /* virtual closure */
  CLOSURE_BSDF_ASHIKHMIN_SHIRLEY_ID,
  CLOSURE_BSDF_ASHIKHMIN_VELVET_ID,
  CLOSURE_BSDF_PHONG_RAMP_ID,
  CLOSURE_BSDF_GLOSSY_TOON_ID,
  CLOSURE_BSDF_HAIR_REFLECTION_ID,

  /* Transmission */
  CLOSURE_BSDF_MICROFACET_BECKMANN_REFRACTION_ID,
  CLOSURE_BSDF_MICROFACET_GGX_REFRACTION_ID,
  CLOSURE_BSDF_HAIR_TRANSMISSION_ID,

  /* Glass */
  CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID,  /* virtual closure */
  CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID,       /* virtual closure */
  CLOSURE_BSDF_MICROFACET_MULTI_GGX_GLASS_ID, /* virtual closure */
  CLOSURE_BSDF_HAIR_CHIANG_ID,
  CLOSURE_BSDF_HAIR_HUANG_ID,
  ...
}

并不是每一个Shader节点都对于一个ClosureType,为了便于使用,一个Shader节点可以包含多个ClosureType切换,比如下面这个Toon BSDF节点,可以需要切换成Diffuse或Glossy模式,分别对应CLOSURE_BSDF_DIFFUSE_TOON_ID和CLOSURE_BSDF_GLOSSY_TOON_ID这两个ClosureType。

在一个Shader Graph中可以包含多个闭包,它们以不同的权重混合,比如下面这个Shader Graph包含了两个闭包,

在SVM中,Shader Graph会被打包成一个一个的闭包,并且将它们以二叉树的形式组织起来。

成员变量及初始化

它有以下成员变量,

class ShaderGraph : public NodeOwner {
 public:
  list<ShaderNode *> nodes;
  size_t num_node_ids;
  bool finalized;
  bool simplified;
  string displacement_hash;
 ...
};

nodes是Shader Graph中所有的Shader Node。
num_node_ids记录Shader Node数量。
finalized标识此Shader Graph是否已经确定,无法再进行修改。
simplified标识此Shader Graph是否已被简化过。
Shader Graph类的初始化,

ShaderGraph::ShaderGraph()
{
  finalized = false;
  simplified = false;
  num_node_ids = 0;
  add(create_node<OutputNode>());
}

初始化一个空的Shader Graph时,至少会添加一个OutputNode节点。

重要的成员函数

添加节点

ShaderNode *ShaderGraph::add(ShaderNode *node)
{
  assert(!finalized);
  simplified = false;

  node->id = num_node_ids++;
  nodes.push_back(node);
  return node;
}

向Shader Graph中添加一个节点时,需要保证 finalized为false,并且要将simplified置为false。从这里也可以看出,Shader Node的id随着node数量的增加而递增,正好对应它在nodes中的索引。

Shader Graph简化

void ShaderGraph::simplify(Scene *scene)
{
  if (!simplified) {
    expand();
    default_inputs(scene->shader_manager->use_osl());
    clean(scene);
    refine_bump_nodes();

    simplified = true;
  }
}

针对Shader Graph进行优化,包括了展开节点,读取节点默认值,删除未连接的节点等操作,由于这个步骤对SVM执行的流程影响不大,不再详细展开。

Shader Graph finalize

void ShaderGraph::finalize(Scene *scene, bool do_bump, bool bump_in_object_space)
{
  /* before compiling, the shader graph may undergo a number of modifications.
   * currently we set default geometry shader inputs, and create automatic bump
   * from displacement. a graph can be finalized only once, and should not be
   * modified afterwards. */

  if (!finalized) {
    simplify(scene);

    if (do_bump) {
      bump_from_displacement(bump_in_object_space);
    }

    ShaderInput *surface_in = output()->input("Surface");
    ShaderInput *volume_in = output()->input("Volume");

    /* todo: make this work when surface and volume closures are tangled up */

    if (surface_in->link) {
      transform_multi_closure(surface_in->link->parent, NULL, false);
    }
    if (volume_in->link) {
      transform_multi_closure(volume_in->link->parent, NULL, true);
    }

    finalized = true;
  }
}

这个函数的描述中说明,每个Shader Graph的finalize只能由一次,并且finalize后就不能再修改这个Shader Graph了,忽略bump map的部分,finalize会自动先执行一次simplify来简化Shader Graph,然后执行transform_multi_closure,这里将transform_multi_closure分成了Surface和Volume两种情况,由于大多数情况下都是使用Surface渲染,这里就只考虑Surface的情况。

transform_multi_closure函数源码片段:

void ShaderGraph::transform_multi_closure(ShaderNode *node, ShaderOutput *weight_out, bool volume)
{
  /* for SVM in multi closure mode, this transforms the shader mix/add part of
   * the graph into nodes that feed weights into closure nodes. this is too
   * avoid building a closure tree and then flattening it, and instead write it
   * directly to an array */

  if (node->special_type == SHADER_SPECIAL_TYPE_COMBINE_CLOSURE) {
    ...
  }
  else {
    ShaderInput *weight_in = node->input((volume) ? "VolumeMixWeight" : "SurfaceMixWeight");

    /* not a closure node? */
    if (!weight_in) {
      return;
    }
    ...
  }
}

在这个函数的说明里,这个步骤用于在多个closure的情况下将Mix Shader和Add Shader节点转换为权重节点(不同的closure只会以这两个节点来混合)。

从此函数被调用的方式来看,node参数必然是一个closure(Shader类型的节点),在第一个条件判断中,先判断nodespecial_type == SHADER_SPECIAL_TYPE_COMBINE_CLOSURE,这所有的节点中,只有Mix Shader和Add Shader两个节点的special_type是SHADER_SPECIAL_TYPE_COMBINE_CLOSURE,所以这里是为了判断Shader Graph中是否使用了这两个节点。

大部分情况下Shader Graph中是不会同时使用Mix Shader和Add Shader来混合closure的,所以这里主要看没有这两个节点的情况。

在没有混合closure的情况下,第一步先取出了node的SurfaceMixWeight输入槽(主要考虑Surface渲染的情况)。这个输入槽只有closure才有,但是在Blender中并没有将它暴露出来,可能是内部使用的输入。

void ShaderGraph::transform_multi_closure(ShaderNode *node, ShaderOutput *weight_out, bool volume)
{
  if (node->special_type == SHADER_SPECIAL_TYPE_COMBINE_CLOSURE) {
    ...
  }
  else {
    ...
    /* not a closure node? */
    if (!weight_in) {
      return;
    }

    /* already has a weight connected to it? add weights */
    float weight_value = node->get_float(weight_in->socket_type);
    if (weight_in->link || weight_value != 0.0f) {
      MathNode *math_node = create_node<MathNode>();
      add(math_node);

      if (weight_in->link) {
        connect(weight_in->link, math_node->input("Value1"));
      }
      else {
        math_node->set_value1(weight_value);
      }

      if (weight_out) {
        connect(weight_out, math_node->input("Value2"));
      }
      else {
        math_node->set_value2(1.0f);
      }

      weight_out = math_node->output("Value");
      if (weight_in->link) {
        disconnect(weight_in);
      }
    }

    /* connected to closure mix weight */
    if (weight_out) {
      connect(weight_out, weight_in);
    }
    else {
      node->set(weight_in->socket_type, weight_value + 1.0f);
    }
  }
}

如果SurfaceMixWeight输入槽有外部连接或者有值的话,下面的操作是加入一个MathNode,MathNode默认情况下运算法则为 output = value1 + value2。然后把MathNode加入到SurfaceMixWeight和它的输入节点之前,MathNode输入为SurfaceMixWeight之前接受的输入和函数的入参weight_out,MathNode的输出作为SurfaceMixWeight的新输入。总体来说就是把SurfaceMixWeight的值和函数入参weight_out相加,得到的结果作为SurfaceMixWeight的新值。

目前这个Shader Graph的finalize操作有点难以理解,首先所有closure的SurfaceMixWeight输入槽都是未开放的,几乎不会有向SurfaceMixWeight连接的节点,其次这个加入MathNode的操作不理解有什么用,既然是想要修改SurfaceMixWeight输入的值,为什么不在这里直接计算,毕竟finalize后这个Shader Graph就不能再改变了。

这样设计原因猜想:SurfaceMixWeight输入并不对用户开放,而是在内部处理Shader Graph时才使用的,这里将SurfaceMixWeight的输入加上了weight_out(如果weight_out传入的时nullptr,那就加上1)是因为SurfaceMixWeight默认输入为0,在之前对SurfaceMixWeight进行操作时并未对它赋值,所以这里才是对它的初始化。如果使用了Mix Shader和Add Shader,这里需要将其替换掉,因为这两个节点在SVM中有其他实现,提供这两个节点只是为了保持与Blender的统一。
在SVM编译时,MixClosureNode没有做任何事。

void MixClosureNode::compile(SVMCompiler & /*compiler*/)
{
  /* handled in the SVM compiler */
}

获取closure的数量

int ShaderGraph::get_num_closures()
{
  int num_closures = 0;
  foreach (ShaderNode *node, nodes) {
    ClosureType closure_type = node->get_closure_type();
    if (closure_type == CLOSURE_NONE_ID) {
      continue;
    }
    else if (CLOSURE_IS_BSSRDF(closure_type)) {
      num_closures += 3;
    }
    else if (CLOSURE_IS_BSDF_MULTISCATTER(closure_type)) {
      num_closures += 2;
    }
    else if (CLOSURE_IS_PRINCIPLED(closure_type)) {
      num_closures += 12;
    }
    else if (CLOSURE_IS_VOLUME(closure_type)) {
      /* TODO(sergey): Verify this is still needed, since we have special minimized volume storage
       * for the volume steps. */
      num_closures += MAX_VOLUME_STACK_SIZE;
    }
    else if (closure_type == CLOSURE_BSDF_MICROFACET_BECKMANN_GLASS_ID ||
             closure_type == CLOSURE_BSDF_MICROFACET_GGX_GLASS_ID ||
             closure_type == CLOSURE_BSDF_HAIR_CHIANG_ID ||
             closure_type == CLOSURE_BSDF_HAIR_HUANG_ID)
    {
      num_closures += 2;
    }
    else {
      ++num_closures;
    }
  }
  return num_closures;
}

这个函数统计Shader Graph中closure的数量,关于节点的get_closure_type函数在closure章节中有说明,从这个函数中可以看出,并不是所有类型的closure都是为一个closure,比如BSSRDF的closure视为3个closure,最常用的BSDF_PRINCIPLED视为12个closure。

Shader

Shader直接决定了场景中物体、背景甚至灯光的外观,它是Shader Graph的更高一级封装,每个Shader除了包含一个Shader Graph之外,还包含了关于Shader Graph的一些附加信息,比如是否使用surface shader,这些信息将会影响到Shader的计算方式。

class Shader : public Node {
 public:
  ...
  
  /* shader graph */
  ShaderGraph *graph;
  ...
  
  /* information about shader after compiling */
  bool has_surface;
  bool has_surface_transparent;
  bool has_surface_raytrace;
  ...
};

Shader中关于Shader Graph的附加信息将在compile后得到。

SVMCompiler

通过前面对Shader系统中各中概念的了解,这里才正式开始接触SVM。
SVMCompiler是SVM的编译器,用于将ShaderGraph编译成便于执行的形式,这个编译并非指计算机程序的编译,只是对ShaderGraph进行进一步的处理,以便于在GPU上也能执行它。

下面是代码中针对SVM的描述注释:
Shder可以看作一些节点的集合,执行Shder就是根据节点计数器顺序地执行这些节点,每个节点关联的数据都是以uint4(4个unsigned int)的形式来储存的,如果一个节点关联的数据超过了这个范围,可以增加节点计数器来适应它(类似将它看作两个节点)。
所有节点的输出都保存在一个stack中,因为节点输出都是color, vector, factor这三种数据,所以栈中的数据都是float类型。stack保存在GPU内部。
shader的执行结果是一个closure,包括它的类型,标签,数据,权重。通过混合closure节点支持对多个closure进行采样,这个逻辑在SVM Compiler中处理。

SVMCompiler用于将Shader编译成多个SVM Node,和Shader Node不同,SVM Node在kernel中定义,并且直接可以在GPU上执行。

内置的数据结构

在SVMCompiler中,定义了下面两个数据结构。

stack

  struct Stack {
    Stack()
    {
      memset(users, 0, sizeof(users));
    }
    Stack(const Stack &other)
    {
      memcpy(users, other.users, sizeof(users));
    }
    Stack &operator=(const Stack &other)
    {
      memcpy(users, other.users, sizeof(users));
      return *this;
    }

    bool empty()
    {
      for (int i = 0; i < SVM_STACK_SIZE; i++) {
        if (users[i]) {
          return false;
        }
      }

      return true;
    }
   int users[SVM_STACK_SIZE];
};

stack虽然叫stack,它本质是一个int的数组,SVM_STACK_SIZE = 255。
SVM中提供了与stack配套的操作函数

  int stack_size(SocketType::Type type);
  int stack_assign(ShaderOutput *output);
  int stack_assign(ShaderInput *input);
  int stack_assign_if_linked(ShaderInput *input);
  int stack_assign_if_linked(ShaderOutput *output);
  int stack_find_offset(int size);
  int stack_find_offset(SocketType::Type type);
  void stack_clear_offset(SocketType::Type type, int offset);
  void stack_link(ShaderInput *input, ShaderOutput *output);

stack_size确认不同Type的数据在stack中占用的元素大小,float和int数据占用1个元素,color, vector, point等占用3个元素。
stack_find_offset(SocketType::Type type)调用stack_find_offset(int size),在svm的active_stack中找出可以存放这种数据类型的位置,比如color类型的数据需要3个元素存放。

int SVMCompiler::stack_find_offset(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;
    }
  }
  ...
  return 0;
}

在active_stack中找到可以存放该类型数据的位置后,会将active_stack中对应的元素置一,并且返回该位置的起始offset。
stack_assign将ShaderOutput或ShaderInput存入stack中,并会根据需要生成SVM Node,存入ShaderInput的方法如下:

int SVMCompiler::stack_assign(ShaderInput *input)
{
  /* stack offset assign? */
  if (input->stack_offset == SVM_STACK_INVALID) {
    if (input->link) {
      /* linked to output -> use output offset */
      ...
    }
    else {
    ...
  }

  return input->stack_offset;
}

首先,存入的ShaderInput的stack_offset必须为SVM_STACK_INVALID,这是ShaderInput的stack_offset默认值,这也就说明一个ShaderInput对象只能存入一次stack。
其次,针对该ShaderInput是否被连接采取不同的存入方法,下面分别讨论这两种情况:
该ShaderInput与一个ShaderOutput连接时:

      /* linked to output -> use output offset */
      assert(input->link->stack_offset != SVM_STACK_INVALID);
      input->stack_offset = input->link->stack_offset;

直接使用它连接的ShaderOutput的stack_offset,这很容易理解,因为ShaderInput的值就是与它连接的ShaderOutput的值。
该ShaderInput未做任何连接:

      Node *node = input->parent;

      /* not linked to output -> add nodes to load default value */
      input->stack_offset = stack_find_offset(input->type());

      if (input->type() == SocketType::FLOAT) {
        add_node(NODE_VALUE_F,
                 __float_as_int(node->get_float(input->socket_type)),
                 input->stack_offset);
      }
      else if (input->type() == SocketType::INT) {
        add_node(NODE_VALUE_F, 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_node(NODE_VALUE_V, input->stack_offset);
        add_node(NODE_VALUE_V, node->get_float3(input->socket_type));
      }
      else { /* should not get called for closure */
        assert(0);
      }

首先调用stack_find_offset在stack中找到该ShaderInput类型的offset,然后根据该ShaderInput类型生成对应的SVM Node,add_node函数用于向compile结果中添加新的SVM Node,比如add_node(NODE_VALUE_F, value1, value2)就是添加了一个NODE_VALUE_F节点,并且它关联的数据是value1和value2,它的实现在后续说明。
这说明假设节点的某一个输入没有连接到其他节点的输出,而是直接输入了一个数时,SVM会为它自动生成Value节点,如果时float数据,就生成一个NODE_VALUE_F,如果时Vector(或Color)数据,就生成两个NODE_VALUE_V。
这种方式保证了Shader Graph编译后的第一个SVM Node一定是NODE_VALUE_XX类型的节点。

将ShaderOutput存入stack的方法如下:

int 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->type());
  }

  return output->stack_offset;
}

直接在stack中找到该ShaderOutput类型的offset,因为ShaderOutput的值必然是从SVM Node中计算或读取出来的,只需要为它找到储存的位置即可。

CompilerState

 /* Global state of the compiler accessible from the compilation routines. */
  struct CompilerState {
    explicit CompilerState(ShaderGraph *graph);

    /* ** Global state, used by various compilation steps. ** */

    /* Set of nodes which were already compiled. */
    ShaderNodeSet nodes_done;

    /* Set of closures which were already compiled. */
    ShaderNodeSet closure_done;

    /* Set of nodes used for writing AOVs. */
    ShaderNodeSet aov_nodes;

    /* ** SVM nodes generation state ** */

    /* Flag whether the node with corresponding ID was already compiled or
     * not. Array element with index i corresponds to a node with such if.
     */
    vector<bool> nodes_done_flag;

    /* Node features that can be compiled. */
    uint node_feature_mask;
  };

从注释上可以看出,CompilerState是complie期间的全局变量,通过它可以访问complie时的状态,主要有nodes_done,保存已编译完毕的节点;closure_done,保存已编译完毕的closure。

成员变量

public

  Scene *scene;
  ShaderGraph *current_graph;
  bool background;

scene:初始化SVMCompile使用的场景。
current_graph:当前compile的ShaderGraph,从下面的current_shader成员中获得。
background:标记当前正在compile的Shader是否为background。

protected

  std::atomic_int *svm_node_types_used;
  array<int4> current_svm_nodes;
  ShaderType current_type;
  Shader *current_shader;
  Stack active_stack;
  int max_stack_use;
  uint mix_weight_offset;
  uint bump_state_offset;
  bool compile_failed;

svm_node_types_used:这是一个bitmap,标记所有使用的SVM Node类型,因为一个场景中的ShaderGraph是并行编译的,所以它的类型是std::atomic_int。
current_svm_nodes:current_graph编译得到的所有SVM Node,每个SVM Node都以一个int4数据保存。
current_type:current_shader的type,默认为SHADER_TYPE_SURFACE。
current_shader:当前编译的Shader,一个场景中可能有多个shader。
active_stack:编译中使用的stack。
max_stack_use:记录使用的最大stack容量。
compile_failed:标记是否compile失败。

compile

compile函数就是SVMCompile最核心的函数,它用于将场景中的Shader编译成多个SVM node,在GPU上就可以按照顺序执行这些SVM node了。
compile函数源码片段:

void SVMCompiler::compile(Shader *shader, array<int4> &svm_nodes, int index, Summary *summary)
{
  svm_node_types_used[NODE_SHADER_JUMP] = true;
  svm_nodes.push_back_slow(make_int4(NODE_SHADER_JUMP, 0, 0, 0));

  /* copy graph for shader with bump mapping */
  ShaderNode *output = shader->graph->output();
  int start_num_svm_nodes = svm_nodes.size();
  ...
  bool has_bump = (shader->get_displacement_method() != DISPLACE_TRUE) &&
                  output->input("Surface")->link && output->input("Displacement")->link;

  /* finalize */
  {
    scoped_timer timer((summary != NULL) ? &summary->time_finalize : NULL);
    shader->graph->finalize(scene, has_bump, shader->get_displacement_method() == DISPLACE_BOTH);
  }

  current_shader = shader;
  ...
}

compile开始时,首先向编译结果svm_nodes中加入一个NODE_SHADER_JUMP节点,这个节关联的数据是(0, 0, 0),加入此节点的作用后续会解释。
然后就是一些变量的初始化和信息提取,不再赘述。Shader Graph的finalize操作在这里完成。
compile函数源码片段:

void SVMCompiler::compile(Shader *shader, array<int4> &svm_nodes, int index, Summary *summary)
{
  ...
  current_shader = shader;

  shader->has_surface = false;
  shader->has_surface_transparent = false;
  shader->has_surface_raytrace = false;
  shader->has_surface_bssrdf = false;
  shader->has_bump = has_bump;
  shader->has_bssrdf_bump = has_bump;
  shader->has_volume = false;
  shader->has_displacement = false;
  shader->has_surface_spatial_varying = false;
  shader->has_volume_spatial_varying = false;
  shader->has_volume_attribute_dependency = false;
  ...
}

这里是对shader的初始化,现在很多成员都是false,后面在编译的时候会根据实际情况填充这些数据。
compile函数源码片段:

void SVMCompiler::compile(Shader *shader, array<int4> &svm_nodes, int index, Summary *summary)
{

  /* generate bump shader */
  ...

  /* generate surface shader */
  {
    scoped_timer timer((summary != NULL) ? &summary->time_generate_surface : NULL);
    compile_type(shader, shader->graph, 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) {
      svm_nodes[index].y = svm_nodes.size();
    }
    svm_nodes.append(current_svm_nodes);
  }

  /* generate volume shader */
  ...

  /* generate displacement shader */
  ...

  /* Fill in summary information. */
  ...

  /* Estimate emission for MIS. */
  ...
}

这里就是针对不同的shader类型进行具体的编译了,目前只关注surface shader,也就是最常用的表面着色。可以看到,这里是调用了另一个函数compile_type,它根据不同的shader类型执行不同的编译流程。
编译完成后,如果没有使用bump节点(多数情况是未使用的),需要将svm_nodes[index]节点的y参数改为svm_nodes.size()(也就之前编译已生成的SVM Node数量,或者说此次生成的SVM Node的起始位置),通常情况下index的值为0(目前还没有发现不为0的情况),也就是说svm_nodes[index]是第一个SVM Node。上面提到过,在compile开始时,在svm_nodes中会先插入一个NODE_SHADER_JUMP节点,所以第一个SVM Node确定是一个NODE_SHADER_JUMP,这里就是将该NODE_SHADER_JUMP的y参数改为svm_nodes.size()。
最后,将当前shader编译的结果加入到svm_nodes中。

需要注意的是,这里generate shader的流程中,只有bump shader需要根据has_bump来决定是否执行,其余类型的shader都是顺序执行的,也就是说,一个shader可能编译出包含多种shader类型的SVM Nodes,比如同时包含surface shader和volume shader。在volume shader中,svm_nodes.size()保存在NODE_SHADER_JUMP节点的z参数上,在displacement shader中,svm_nodes.size()保存在NODE_SHADER_JUMP节点的w参数上。从这里就可以大概推断出NODE_SHADER_JUMP节点的功能,如果一个shder中包含了多个shader类型,NODE_SHADER_JUMP中的数据保存了每个shader类型编译成的SVM Node的起始位置,后面就可以根据这个位置取出每个shader类型对应的SVM Node。

compile_type

根据不同的shader类型执行不同的编译流程。
compile_type源码片段:

void SVMCompiler::compile_type(Shader *shader, ShaderGraph *graph, ShaderType type)
{  
  current_type = type;
  current_graph = graph;

  /* get input in output node */
  ShaderNode *output = graph->output();
  ShaderInput *clin = NULL;

  switch (type) {
    case SHADER_TYPE_SURFACE:
      clin = output->input("Surface");
      break;
    case SHADER_TYPE_VOLUME:
      ...
    default:
      assert(0);
      break;
  }
...
}

初始化current_type和current_graph,然后从Output节点中取出与其相连的ShaderInput,通过这个ShaderInput是Output节点的哪个输入就可以判断此shader的类型,比如输入在Surface上就说明此shader类型为 Surface Sahder。
compile_type源码片段:

void SVMCompiler::compile_type(Shader *shader, ShaderGraph *graph, ShaderType type)
{
  ...
  /* clear all compiler state */
  memset((void *)&active_stack, 0, sizeof(active_stack));
  current_svm_nodes.clear();

  foreach (ShaderNode *node, graph->nodes) {
    foreach (ShaderInput *input, node->inputs)
      input->stack_offset = SVM_STACK_INVALID;
    foreach (ShaderOutput *output, node->outputs)
      output->stack_offset = SVM_STACK_INVALID;
  }
  ...
}

编译前的初始化,将active_stack和current_svm_nodes清空,并且将所有节点的ShaderInput和ShaderOutput的stack_offset置为SVM_STACK_INVALID。
compile_type源码片段:

void SVMCompiler::compile_type(Shader *shader, ShaderGraph *graph, ShaderType type)
{
  ...  
  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 (clin->link) {
          shader->has_surface = true;
          state.node_feature_mask = KERNEL_FEATURE_NODE_MASK_SURFACE;
        }
        break;
      case SHADER_TYPE_VOLUME: /* generate volume shader */
        ...
      default:
        break;
    }
  ...
}

只有reference_count不为0的shader才会被编译,也就是确实在场景中使用了的shader。针对Surface Shader,将shader中的has_surface置为true。其他几种shader同理。
find_aov_nodes_and_dependencies是为了处理shader中的aov节点,它用于将shader中的一些计算结果发送至compositing中,可用于一些后处理算法,这里暂时将其忽略。
compile_type源码片段:

void SVMCompiler::compile_type(Shader *shader, ShaderGraph *graph, ShaderType type)
{
  ... 
  if (clin->link) {
    generate_multi_closure(clin->link->parent, clin->link->parent, &state);
  }

  /* compile output node */
  output->compile(*this);
  ...
  if (type != SHADER_TYPE_BUMP) {
    add_node(NODE_END, 0, 0, 0);
  }
} 

调用generate_multi_closure生成closure,根据Shader生成SVM Nodes。生成完成后,最后处理output节点,这里调用的outputcompile就是每个节点自定义的如何将此节点转换为SVM Node的方法,在generate_multi_closure中最终也是使用这个方法来转换各个Shader Node。所有处理完成后,如果此Shader不是SHADER_TYPE_BUMP,就为此Shader加上一个NODE_END节点,标记Shader的结束。

generate_multi_closure

转换Shader中的各个Shader Node,这个转换以每一个closure为单位来执行。

void SVMCompiler::generate_multi_closure(ShaderNode *root_node,
                                         ShaderNode *node,
                                         CompilerState *state)
{
  /* only generate once */
  if (state->closure_done.find(node) != state->closure_done.end()) {
    return;
  }

  state->closure_done.insert(node);

  if (node->special_type == SHADER_SPECIAL_TYPE_COMBINE_CLOSURE) {
    ...
  }
  else {
    generate_closure_node(node, state);
  }

  state->nodes_done.insert(node);
  state->nodes_done_flag[node->id] = true;
}

除了将各类信息更新至state中以外,最重要的就是根据node的special_type决定不同的流程,前面已经提到过special_type为SHADER_SPECIAL_TYPE_COMBINE_CLOSURE的只有两个节点,就是Mix Shader和Add Shader,它们用于混合两个不同的closure。这里只考虑简单的情况,所以最终调用的是generate_closure_node。

generate_closure_node

生成closure的SVM Nodes的方法。
generate_closure_node源码片段:

void SVMCompiler::generate_closure_node(ShaderNode *node, CompilerState *state)
{
  ...

  /* execute dependencies for closure */
  foreach (ShaderInput *in, node->inputs) {
    if (in->link != NULL) {
      ShaderNodeSet dependencies;
      find_dependencies(dependencies, state->nodes_done, in);
      generate_svm_nodes(dependencies, state);
    }
  }
  ...
}

遍历节点的每一个ShaderInput,寻找与它们相连的node,然后调用generate_svm_nodes生成这些node的SVM Node。generate_closure_node函数传入的node参数必定是一个closure。
find_dependencies用于从一个ShaderInput中寻找它所依赖的所有Shader Node。
generate_closure_node源码片段:

void SVMCompiler::generate_closure_node(ShaderNode *node, CompilerState *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;
  }
  ...
}

这里取出该closure的weight并将其保存在stack中,这里只有weight有连接或者不为1的时候会保存。这里mix_weight_offset会记录weight的偏移地址,但是没有看到它有什么用。
generate_closure_node源码片段:

void SVMCompiler::generate_closure_node(ShaderNode *node, CompilerState *state)
{  
  ...  
  /* 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 = true;
    }
  }
}

将closure的依赖节点处理完毕后,调用generate_node处理自身,然后根据该closure的一些信息填充current_shader的各个字段,比如has_surface_bssrdf标记此shader是否含有bssrdf。

generate_svm_nodes

输入一个ShaderNodeSet,将其中的所有nodes转换为SVM Node。

void SVMCompiler::generate_svm_nodes(const ShaderNodeSet &nodes, CompilerState *state)
{
  ShaderNodeSet &done = state->nodes_done;
  vector<bool> &done_flag = state->nodes_done_flag;

  bool nodes_done;
  do {
    nodes_done = true;

    foreach (ShaderNode *node, nodes) {
      if (!done_flag[node->id]) {
        bool inputs_done = true;

        foreach (ShaderInput *input, node->inputs) {
          if (input->link && !done_flag[input->link->parent->id]) {
            inputs_done = false;
          }
        }
        if (inputs_done) {
          generate_node(node, done);
          done.insert(node);
          done_flag[node->id] = true;
        }
        else {
          nodes_done = false;
        }
      }
    }
  } while (!nodes_done);
}

循环遍历这个node集合,每一次都从中挑选出Shader Input都已经处理完毕的节点(即该节点的前置节点已经处理完毕),调用generate_node处理该节点,直到所有节点都处理完毕。这样处理是因为要保证Shader中各个节点的执行顺序,在GPU中,节点都保存在一个数组中,当一个节点执行时,需要保证它的前置节点都已经执行完毕,所以这里将处理一个节点时,必须保证该节点的前置节点已被处理并加入到了svm_nodes数组中。

generate_node

节点转换的最后一步,输入一个Shader Node,输出对应的SVM Node。

void SVMCompiler::generate_node(ShaderNode *node, ShaderNodeSet &done)
{
  node->compile(*this);
  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;
    }
  }
}

最核心的一步是nodecompile(*this),调用Shader Node本身的comple方法将自己转换为SVM Node,也就是说每个Shader Node的compile规则是自己定义的。
其中,

  stack_clear_users(node, done);
  stack_clear_temporary(node);

这两个函数用于优化内存的使用,所有节点的使用的数据都保存在stack中,当一个节点生成数据被使用完毕后,保存这块数据的内存都可以被其他节点复用了,这里就是将数据已被使用完毕的stack内存清空,让其他节点也可以使用这块内存。因为处理这个Shader Node时它的前置节点必然已经被处理完毕了,与其相关的内存使用是可以预测的。
最后就是根据这个节点的信息填充current_shader的信息,比如has_surface_raytrace标记当前shader是否包含raytrace节点。

compile流程总结

SVMCompiler的compile函数本质上是将场景中的所有Shader转换成SVM Node集合,让GPU中也可以进行Shader Graph的逻辑计算。compile过程中,首先要区分该shader的类型,根据不同的类型有不同的编译流程,如果一个shader有多个类型,每种类型的编译流程会顺序执行。确定好类型后,以closure为单位对所有节点进行编译,当编译一个节点时,需要保证它所依赖的所有前置节点都已经编译完毕。编译时调用每个节点自己的compile函数,它定义了当前节点是如何转换成一个SVM Node的。
生成SVM Node时,主要包括以下三种数据:

  • SVM Node的类型
  • 它所关联的数据,即SVM Node的具体输入数据和输出数据
  • 它所关联的数据在stack中的存放地址
    这三种数据并不一定同时使用,比如有的节点接受的是别人的输入,它就没有具体的关联数据,只需要保存那些数据在stack中的位置即可,等前面的节点计算完毕,自然可以通过地址取到对应的值,生成的SVM Node时,它的类型必然位于数据的第一个位置(每一个SVM Node都是int4类型,将上面三种数据打包到其中)。

Shader Node的compile函数举例

MIX Node


这个节点用于将两种颜色根据Factor混合成一种颜色。
Compile函数:

void MixNode::compile(SVMCompiler &compiler)
{
  ShaderInput *fac_in = input("Fac");
  ShaderInput *color1_in = input("Color1");
  ShaderInput *color2_in = input("Color2");
  ShaderOutput *color_out = output("Color");

  compiler.add_node(NODE_MIX,
                    compiler.stack_assign(fac_in),
                    compiler.stack_assign(color1_in),
                    compiler.stack_assign(color2_in));
  compiler.add_node(NODE_MIX, mix_type, compiler.stack_assign(color_out));

  if (use_clamp) {
    ...
  }
}

上面使用compiler.add_node加入了两个SVM Node,两个都是NODE_MIX,第一个关联的数据为fac_in,color1_in,color2_in 三个输入槽,第二个关联mix_type和color_out两个输出槽。从这里可以看出,一个Shader Node并不是对应一个SVM Node。
关于compiler.add_node函数,它的原型如下:

  void add_node(ShaderNodeType type, int a = 0, int b = 0, int c = 0);
  void add_node(int a = 0, int b = 0, int c = 0, int d = 0);
  void add_node(ShaderNodeType type, const float3 &f);
  void add_node(const float4 &f);

ShaderNodeType是一个枚举,等同于一个unsigned int。
部分源码:

void SVMCompiler::add_node(int a, int b, int c, int d)
{
  current_svm_nodes.push_back_slow(make_int4(a, b, c, d));
}

void SVMCompiler::add_node(ShaderNodeType type, int a, int b, int c)
{
  svm_node_types_used[type] = true;
  current_svm_nodes.push_back_slow(make_int4(type, a, b, c));
}

add_node函数就是将输入的4个int或float数据打包成一个int4,然后push到current_svm_nodes中。

Brightness/Contrast Node


用于调整输入颜色的亮度和对比度。

void BrightContrastNode::compile(SVMCompiler &compiler)
{
  ShaderInput *color_in = input("Color");
  ShaderInput *bright_in = input("Bright");
  ShaderInput *contrast_in = input("Contrast");
  ShaderOutput *color_out = output("Color");

  compiler.add_node(NODE_BRIGHTCONTRAST,
                    compiler.stack_assign(color_in),
                    compiler.stack_assign(color_out),
                    compiler.encode_uchar4(compiler.stack_assign(bright_in),
                                           compiler.stack_assign(contrast_in)));
}

编译时向current_svm_nodes中加入NODE_BRIGHTCONTRAST,关联的数据为color_in, color_out, bright_in, contrast_in。这里将bright_in,contrast_in打包到了一个int(uchar4)中,因为它们的值都小于255,用一个字节就可以表示。