Cycles: add unit tests for supported constant folding rules.
authorAlexander Gavrilov <angavrilov@gmail.com>
Mon, 1 Aug 2016 15:53:20 +0000 (18:53 +0300)
committerAlexander Gavrilov <angavrilov@gmail.com>
Mon, 1 Aug 2016 15:53:20 +0000 (18:53 +0300)
Code coverage of different combinations of secondary conditions
is obviously not complete because there are so many of them, but
all main rules should be there.

The reason for CORRECT vs INVALID is that both words have the same
number of characters so calls line up, but look quite different.

Reviewers: #cycles, sergey

Reviewed By: #cycles, sergey

Subscribers: dingto, sergey, brecht

Differential Revision: https://developer.blender.org/D2130

intern/cycles/render/constant_fold.cpp
intern/cycles/test/CMakeLists.txt
intern/cycles/test/render_graph_finalize_test.cpp

index 073bafce98d31b4ab41922a52c4460be046d65b7..200a4c497cdc037f1aeb37d44eb780a6cb4488e1 100644 (file)
@@ -40,7 +40,8 @@ bool ConstantFolder::all_inputs_constant() const
 
 void ConstantFolder::make_constant(float value) const
 {
-       VLOG(1) << "Replacing " << node->name << " with constant " << value << ".";
+       VLOG(1) << "Folding " << node->name << "::" << output->name() << " to constant (" << value << ").";
+
        foreach(ShaderInput *sock, output->links) {
                sock->set(value);
        }
@@ -50,6 +51,8 @@ void ConstantFolder::make_constant(float value) const
 
 void ConstantFolder::make_constant(float3 value) const
 {
+       VLOG(1) << "Folding " << node->name << "::" << output->name() << " to constant " << value << ".";
+
        foreach(ShaderInput *sock, output->links) {
                sock->set(value);
        }
@@ -90,6 +93,8 @@ void ConstantFolder::bypass(ShaderOutput *new_output) const
 {
        assert(new_output);
 
+       VLOG(1) << "Folding " << node->name << "::" << output->name() << " to socket " << new_output->parent->name << "::" << new_output->name() << ".";
+
        /* Remove all outgoing links from socket and connect them to new_output instead.
         * The graph->relink method affects node inputs, so it's not safe to use in constant
         * folding if the node has multiple outputs and will thus be folded multiple times. */
@@ -105,6 +110,9 @@ void ConstantFolder::bypass(ShaderOutput *new_output) const
 void ConstantFolder::discard() const
 {
        assert(output->type() == SocketType::CLOSURE);
+
+       VLOG(1) << "Discarding closure " << node->name << ".";
+
        graph->disconnect(output);
 }
 
index a6bcf980df21d75242baf6bbbc617d95d1cdb3a6..80fe893826a3c98bb826df6b8116b61964b3afc6 100644 (file)
@@ -26,12 +26,12 @@ set(ALL_CYCLES_LIBRARIES
        cycles_device
        cycles_bvh
        cycles_graph
-       cycles_kernel_osl
        cycles_util
        ${OPENIMAGEIO_LIBRARIES}
 )
 if(WITH_CYCLES_OSL)
        list(APPEND ALL_CYCLES_LIBRARIES
+               cycles_kernel_osl
                ${OSL_LIBRARIES}
                ${LLVM_LIBRARIES}
        )
index 4566894d490212ad5225eeb1062ee0de2b76676d..e329384021a0beb4d4b4bfbb425a5857bace71c7 100644 (file)
@@ -40,6 +40,7 @@ public:
          : name_(name)
        {
                node_ = new T();
+               node_->name = name;
        }
 
        const string& name() const {
@@ -59,6 +60,13 @@ public:
                return *this;
        }
 
+       template<typename T2, typename V>
+       ShaderNodeBuilder& set(V T2::*pfield, V value)
+       {
+               static_cast<T*>(node_)->*pfield = value;
+               return *this;
+       }
+
 protected:
        string name_;
        ShaderNode *node_;
@@ -69,6 +77,7 @@ public:
        explicit ShaderGraphBuilder(ShaderGraph *graph)
          : graph_(graph)
        {
+               node_map_["Output"] = graph->output();
        }
 
        ShaderNode *find_node(const string& name)
@@ -110,6 +119,27 @@ public:
                return *this;
        }
 
+       /* Common input/output boilerplate. */
+       ShaderGraphBuilder& add_attribute(const string &name)
+       {
+               return (*this)
+                       .add_node(ShaderNodeBuilder<AttributeNode>(name)
+                                 .set(&AttributeNode::attribute, ustring(name)));
+       }
+
+       ShaderGraphBuilder& output_closure(const string& from)
+       {
+               return (*this).add_connection(from, "Output::Surface");
+       }
+
+       ShaderGraphBuilder& output_color(const string& from)
+       {
+               return (*this)
+                       .add_node(ShaderNodeBuilder<EmissionNode>("EmissionNode"))
+                       .add_connection(from, "EmissionNode::Color")
+                       .output_closure("EmissionNode::Emission");
+       }
+
 protected:
        ShaderGraph *graph_;
        map<string, ShaderNode *> node_map_;
@@ -127,21 +157,1297 @@ protected:
        ShaderGraph graph; \
        ShaderGraphBuilder builder(&graph); \
 
+#define EXPECT_ANY_MESSAGE(log) \
+       EXPECT_CALL(log, Log(_, _, _)).Times(AnyNumber()); \
+
+#define CORRECT_INFO_MESSAGE(log, message) \
+       EXPECT_CALL(log, Log(google::INFO, _, HasSubstr(message)));
+
+#define INVALID_INFO_MESSAGE(log, message) \
+       EXPECT_CALL(log, Log(google::INFO, _, HasSubstr(message))).Times(0);
+
+/*
+ * Test deduplication of nodes that have inputs, some of them folded.
+ */
+TEST(render_graph, deduplicate_deep)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Value1::Value to constant (0.8).");
+       CORRECT_INFO_MESSAGE(log, "Folding Value2::Value to constant (0.8).");
+
+       builder
+               .add_node(ShaderNodeBuilder<GeometryNode>("Geometry1"))
+               .add_node(ShaderNodeBuilder<GeometryNode>("Geometry2"))
+               .add_node(ShaderNodeBuilder<ValueNode>("Value1")
+                         .set(&ValueNode::value, 0.8f))
+               .add_node(ShaderNodeBuilder<ValueNode>("Value2")
+                         .set(&ValueNode::value, 0.8f))
+               .add_node(ShaderNodeBuilder<NoiseTextureNode>("Noise1"))
+               .add_node(ShaderNodeBuilder<NoiseTextureNode>("Noise2"))
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_BLEND)
+                         .set("Fac", 0.5f))
+               .add_connection("Geometry1::Parametric", "Noise1::Vector")
+               .add_connection("Value1::Value", "Noise1::Scale")
+               .add_connection("Noise1::Color", "Mix::Color1")
+               .add_connection("Geometry2::Parametric", "Noise2::Vector")
+               .add_connection("Value2::Value", "Noise2::Scale")
+               .add_connection("Noise2::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+
+       EXPECT_EQ(graph.nodes.size(), 5);
+}
+
+/*
+ * Test RGB to BW node.
+ */
 TEST(render_graph, constant_fold_rgb_to_bw)
 {
        DEFINE_COMMON_VARIABLES(builder, log);
 
-       EXPECT_CALL(log, Log(_, _, _)).Times(AnyNumber());
-       EXPECT_CALL(log, Log(google::INFO, _,
-                            HasSubstr("Replacing rgb_to_bw with constant 0.8.")));
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding RGBToBWNodeNode::Val to constant (0.8).");
+       CORRECT_INFO_MESSAGE(log, "Folding convert_float_to_color::value_color to constant (0.8, 0.8, 0.8).");
 
        builder
-               .add_node(ShaderNodeBuilder<OutputNode>("OutputNode"))
-               .add_node(ShaderNodeBuilder<EmissionNode>("EmissionNode"))
                .add_node(ShaderNodeBuilder<RGBToBWNode>("RGBToBWNodeNode")
                          .set("Color", make_float3(0.8f, 0.8f, 0.8f)))
-               .add_connection("RGBToBWNodeNode::Val", "EmissionNode::Color")
-               .add_connection("EmissionNode::Emission", "OutputNode::Surface");
+               .output_color("RGBToBWNodeNode::Val");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - folding of Emission nodes that don't emit to nothing.
+ */
+TEST(render_graph, constant_fold_emission1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Discarding closure Emission.");
+
+       builder
+               .add_node(ShaderNodeBuilder<EmissionNode>("Emission")
+                         .set("Color", make_float3(0.0f, 0.0f, 0.0f)))
+               .output_closure("Emission::Emission");
+
+       graph.finalize(&scene);
+}
+
+TEST(render_graph, constant_fold_emission2)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Discarding closure Emission.");
+
+       builder
+               .add_node(ShaderNodeBuilder<EmissionNode>("Emission")
+                         .set("Strength", 0.0f))
+               .output_closure("Emission::Emission");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - folding of Background nodes that don't emit to nothing.
+ */
+TEST(render_graph, constant_fold_background1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Discarding closure Background.");
+
+       builder
+               .add_node(ShaderNodeBuilder<BackgroundNode>("Background")
+                         .set("Color", make_float3(0.0f, 0.0f, 0.0f)))
+               .output_closure("Background::Background");
+
+       graph.finalize(&scene);
+}
+
+TEST(render_graph, constant_fold_background2)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Discarding closure Background.");
+
+       builder
+               .add_node(ShaderNodeBuilder<BackgroundNode>("Background")
+                         .set("Strength", 0.0f))
+               .output_closure("Background::Background");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Add Closure with only one input.
+ */
+TEST(render_graph, constant_fold_shader_add)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding AddClosure1::Closure to socket Diffuse::BSDF.");
+       CORRECT_INFO_MESSAGE(log, "Folding AddClosure2::Closure to socket Diffuse::BSDF.");
+       INVALID_INFO_MESSAGE(log, "Folding AddClosure3");
+
+       builder
+               .add_node(ShaderNodeBuilder<DiffuseBsdfNode>("Diffuse"))
+               .add_node(ShaderNodeBuilder<AddClosureNode>("AddClosure1"))
+               .add_node(ShaderNodeBuilder<AddClosureNode>("AddClosure2"))
+               .add_node(ShaderNodeBuilder<AddClosureNode>("AddClosure3"))
+               .add_connection("Diffuse::BSDF", "AddClosure1::Closure1")
+               .add_connection("Diffuse::BSDF", "AddClosure2::Closure2")
+               .add_connection("AddClosure1::Closure", "AddClosure3::Closure1")
+               .add_connection("AddClosure2::Closure", "AddClosure3::Closure2")
+               .output_closure("AddClosure3::Closure");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Mix Closure with 0 or 1 fac.
+ *  - Folding of Mix Closure with both inputs folded to the same node.
+ */
+TEST(render_graph, constant_fold_shader_mix)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding MixClosure1::Closure to socket Diffuse::BSDF.");
+       CORRECT_INFO_MESSAGE(log, "Folding MixClosure2::Closure to socket Diffuse::BSDF.");
+       CORRECT_INFO_MESSAGE(log, "Folding MixClosure3::Closure to socket Diffuse::BSDF.");
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<DiffuseBsdfNode>("Diffuse"))
+               /* choose left */
+               .add_node(ShaderNodeBuilder<MixClosureNode>("MixClosure1")
+                         .set("Fac", 0.0f))
+               .add_connection("Diffuse::BSDF", "MixClosure1::Closure1")
+               /* choose right */
+               .add_node(ShaderNodeBuilder<MixClosureNode>("MixClosure2")
+                         .set("Fac", 1.0f))
+               .add_connection("Diffuse::BSDF", "MixClosure2::Closure2")
+               /* both inputs folded the same */
+               .add_node(ShaderNodeBuilder<MixClosureNode>("MixClosure3"))
+               .add_connection("Attribute::Fac", "MixClosure3::Fac")
+               .add_connection("MixClosure1::Closure", "MixClosure3::Closure1")
+               .add_connection("MixClosure2::Closure", "MixClosure3::Closure2")
+               .output_closure("MixClosure3::Closure");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Invert with all constant inputs.
+ */
+TEST(render_graph, constant_fold_invert)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Invert::Color to constant (0.68, 0.5, 0.32).");
+
+       builder
+               .add_node(ShaderNodeBuilder<InvertNode>("Invert")
+                         .set("Fac", 0.8f)
+                         .set("Color", make_float3(0.2f, 0.5f, 0.8f)))
+               .output_color("Invert::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Invert with zero Fac.
+ */
+TEST(render_graph, constant_fold_invert_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Invert::Color to socket Attribute::Color.");
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<InvertNode>("Invert")
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute::Color", "Invert::Color")
+               .output_color("Invert::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of MixRGB Add with all constant inputs (clamp false).
+ */
+TEST(render_graph, constant_fold_mix_add)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding MixAdd::Color to constant (0.62, 1.14, 1.42).");
+
+       builder
+               .add_node(ShaderNodeBuilder<MixNode>("MixAdd")
+                         .set(&MixNode::type, NODE_MIX_ADD)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 0.8f)
+                         .set("Color1", make_float3(0.3, 0.5, 0.7))
+                         .set("Color2", make_float3(0.4, 0.8, 0.9)))
+               .output_color("MixAdd::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of MixRGB Add with all constant inputs (clamp true).
+ */
+TEST(render_graph, constant_fold_mix_add_clamp)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding MixAdd::Color to constant (0.62, 1, 1).");
+
+       builder
+               .add_node(ShaderNodeBuilder<MixNode>("MixAdd")
+                         .set(&MixNode::type, NODE_MIX_ADD)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 0.8f)
+                         .set("Color1", make_float3(0.3, 0.5, 0.7))
+                         .set("Color2", make_float3(0.4, 0.8, 0.9)))
+               .output_color("MixAdd::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - No folding on fac 0 for dodge.
+ */
+TEST(render_graph, constant_fold_part_mix_dodge_no_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding ");
+
+       builder
+               .add_attribute("Attribute1")
+               .add_attribute("Attribute2")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_DODGE)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute1::Color", "Mix::Color1")
+               .add_connection("Attribute2::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - No folding on fac 0 for light.
+ */
+TEST(render_graph, constant_fold_part_mix_light_no_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding ");
+
+       builder
+               .add_attribute("Attribute1")
+               .add_attribute("Attribute2")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_LIGHT)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute1::Color", "Mix::Color1")
+               .add_connection("Attribute2::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - No folding on fac 0 for burn.
+ */
+TEST(render_graph, constant_fold_part_mix_burn_no_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding ");
+
+       builder
+               .add_attribute("Attribute1")
+               .add_attribute("Attribute2")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_BURN)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute1::Color", "Mix::Color1")
+               .add_connection("Attribute2::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - No folding on fac 0 for clamped blend.
+ */
+TEST(render_graph, constant_fold_part_mix_blend_clamped_no_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding ");
+
+       builder
+               .add_attribute("Attribute1")
+               .add_attribute("Attribute2")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_BLEND)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute1::Color", "Mix::Color1")
+               .add_connection("Attribute2::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Mix with 0 or 1 Fac.
+ *  - Folding of Mix with both inputs folded to the same node.
+ */
+TEST(render_graph, constant_fold_part_mix_blend)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding MixBlend1::Color to socket Attribute1::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding MixBlend2::Color to socket Attribute1::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding MixBlend3::Color to socket Attribute1::Color.");
+
+       builder
+               .add_attribute("Attribute1")
+               .add_attribute("Attribute2")
+               /* choose left */
+               .add_node(ShaderNodeBuilder<MixNode>("MixBlend1")
+                         .set(&MixNode::type, NODE_MIX_BLEND)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute1::Color", "MixBlend1::Color1")
+               .add_connection("Attribute2::Color", "MixBlend1::Color2")
+               /* choose right */
+               .add_node(ShaderNodeBuilder<MixNode>("MixBlend2")
+                         .set(&MixNode::type, NODE_MIX_BLEND)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Fac", 1.0f))
+               .add_connection("Attribute1::Color", "MixBlend2::Color2")
+               .add_connection("Attribute2::Color", "MixBlend2::Color1")
+               /* both inputs folded to Attribute1 */
+               .add_node(ShaderNodeBuilder<MixNode>("MixBlend3")
+                         .set(&MixNode::type, NODE_MIX_BLEND)
+                         .set(&MixNode::use_clamp, false))
+               .add_connection("Attribute1::Fac", "MixBlend3::Fac")
+               .add_connection("MixBlend1::Color", "MixBlend3::Color1")
+               .add_connection("MixBlend2::Color", "MixBlend3::Color2")
+               .output_color("MixBlend3::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - NOT folding of MixRGB Sub with the same inputs and fac NOT 1.
+ */
+TEST(render_graph, constant_fold_part_mix_sub_same_fac_bad)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding Mix::");
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_SUB)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 0.5f))
+               .add_connection("Attribute::Color", "Mix::Color1")
+               .add_connection("Attribute::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of MixRGB Sub with the same inputs and fac 1.
+ */
+TEST(render_graph, constant_fold_part_mix_sub_same_fac_1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Mix::Color to constant (0, 0, 0).");
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_SUB)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 1.0f))
+               .add_connection("Attribute::Color", "Mix::Color1")
+               .add_connection("Attribute::Color", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Graph for testing partial folds of MixRGB with one constant argument.
+ * Includes 4 tests: constant on each side with fac either unknown or 1.
+ */
+static void build_mix_partial_test_graph(ShaderGraphBuilder &builder, NodeMix type, float3 constval)
+{
+       builder
+               .add_attribute("Attribute")
+               /* constant on the left */
+               .add_node(ShaderNodeBuilder<MixNode>("Mix_Cx_Fx")
+                         .set(&MixNode::type, type)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Color1", constval))
+               .add_node(ShaderNodeBuilder<MixNode>("Mix_Cx_F1")
+                         .set(&MixNode::type, type)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Color1", constval)
+                         .set("Fac", 1.0f))
+               .add_connection("Attribute::Fac", "Mix_Cx_Fx::Fac")
+               .add_connection("Attribute::Color", "Mix_Cx_Fx::Color2")
+               .add_connection("Attribute::Color", "Mix_Cx_F1::Color2")
+               /* constant on the right */
+               .add_node(ShaderNodeBuilder<MixNode>("Mix_xC_Fx")
+                         .set(&MixNode::type, type)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Color2", constval))
+               .add_node(ShaderNodeBuilder<MixNode>("Mix_xC_F1")
+                         .set(&MixNode::type, type)
+                         .set(&MixNode::use_clamp, false)
+                         .set("Color2", constval)
+                         .set("Fac", 1.0f))
+               .add_connection("Attribute::Fac", "Mix_xC_Fx::Fac")
+               .add_connection("Attribute::Color", "Mix_xC_Fx::Color1")
+               .add_connection("Attribute::Color", "Mix_xC_F1::Color1")
+               /* results of actual tests simply added up to connect to output */
+               .add_node(ShaderNodeBuilder<MixNode>("Out12")
+                         .set(&MixNode::type, NODE_MIX_ADD)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 1.0f))
+               .add_node(ShaderNodeBuilder<MixNode>("Out34")
+                         .set(&MixNode::type, NODE_MIX_ADD)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 1.0f))
+               .add_node(ShaderNodeBuilder<MixNode>("Out1234")
+                         .set(&MixNode::type, NODE_MIX_ADD)
+                         .set(&MixNode::use_clamp, true)
+                         .set("Fac", 1.0f))
+               .add_connection("Mix_Cx_Fx::Color", "Out12::Color1")
+               .add_connection("Mix_Cx_F1::Color", "Out12::Color2")
+               .add_connection("Mix_xC_Fx::Color", "Out34::Color1")
+               .add_connection("Mix_xC_F1::Color", "Out34::Color2")
+               .add_connection("Out12::Color", "Out1234::Color1")
+               .add_connection("Out34::Color", "Out1234::Color2")
+               .output_color("Out1234::Color");
+}
+
+/*
+ * Tests: partial folding for RGB Add with known 0.
+ */
+TEST(render_graph, constant_fold_part_mix_add_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* 0 + X (fac 1) == X */
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color to socket Attribute::Color.");
+       /* X + 0 (fac ?) == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color to socket Attribute::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color to socket Attribute::Color.");
+       INVALID_INFO_MESSAGE(log, "Folding Out");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_ADD, make_float3(0, 0, 0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for RGB Sub with known 0.
+ */
+TEST(render_graph, constant_fold_part_mix_sub_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color");
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color");
+       /* X - 0 (fac ?) == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color to socket Attribute::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color to socket Attribute::Color.");
+       INVALID_INFO_MESSAGE(log, "Folding Out");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_SUB, make_float3(0, 0, 0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for RGB Mul with known 1.
+ */
+TEST(render_graph, constant_fold_part_mix_mul_1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* 1 * X (fac 1) == X */
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color to socket Attribute::Color.");
+       /* X * 1 (fac ?) == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color to socket Attribute::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color to socket Attribute::Color.");
+       INVALID_INFO_MESSAGE(log, "Folding Out");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_MUL, make_float3(1, 1, 1));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for RGB Div with known 1.
+ */
+TEST(render_graph, constant_fold_part_mix_div_1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color");
+       INVALID_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color");
+       /* X / 1 (fac ?) == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color to socket Attribute::Color.");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color to socket Attribute::Color.");
+       INVALID_INFO_MESSAGE(log, "Folding Out");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_DIV, make_float3(1, 1, 1));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for RGB Mul with known 0.
+ */
+TEST(render_graph, constant_fold_part_mix_mul_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* 0 * ? (fac ?) == 0 */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color to constant (0, 0, 0).");
+       /* ? * 0 (fac 1) == 0 */
+       INVALID_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color to constant (0, 0, 0).");
+
+       CORRECT_INFO_MESSAGE(log, "Folding Out12::Color to constant (0, 0, 0).");
+       INVALID_INFO_MESSAGE(log, "Folding Out1234");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_MUL, make_float3(0, 0, 0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for RGB Div with known 0.
+ */
+TEST(render_graph, constant_fold_part_mix_div_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* 0 / ? (fac ?) == 0 */
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_Fx::Color to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Mix_Cx_F1::Color to constant (0, 0, 0).");
+       INVALID_INFO_MESSAGE(log, "Folding Mix_xC_Fx::Color");
+       INVALID_INFO_MESSAGE(log, "Folding Mix_xC_F1::Color");
+
+       CORRECT_INFO_MESSAGE(log, "Folding Out12::Color to constant (0, 0, 0).");
+       INVALID_INFO_MESSAGE(log, "Folding Out1234");
+
+       build_mix_partial_test_graph(builder, NODE_MIX_DIV, make_float3(0, 0, 0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Separate/Combine RGB with all constant inputs.
+ */
+TEST(render_graph, constant_fold_separate_combine_rgb)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateRGB::R to constant (0.3).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateRGB::G to constant (0.5).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateRGB::B to constant (0.7).");
+       CORRECT_INFO_MESSAGE(log, "Folding CombineRGB::Image to constant (0.3, 0.5, 0.7).");
+
+       builder
+               .add_node(ShaderNodeBuilder<SeparateRGBNode>("SeparateRGB")
+                         .set("Image", make_float3(0.3f, 0.5f, 0.7f)))
+               .add_node(ShaderNodeBuilder<CombineRGBNode>("CombineRGB"))
+               .add_connection("SeparateRGB::R", "CombineRGB::R")
+               .add_connection("SeparateRGB::G", "CombineRGB::G")
+               .add_connection("SeparateRGB::B", "CombineRGB::B")
+               .output_color("CombineRGB::Image");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Separate/Combine XYZ with all constant inputs.
+ */
+TEST(render_graph, constant_fold_separate_combine_xyz)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateXYZ::X to constant (0.3).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateXYZ::Y to constant (0.5).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateXYZ::Z to constant (0.7).");
+       CORRECT_INFO_MESSAGE(log, "Folding CombineXYZ::Vector to constant (0.3, 0.5, 0.7).");
+       CORRECT_INFO_MESSAGE(log, "Folding convert_vector_to_color::value_color to constant (0.3, 0.5, 0.7).");
+
+       builder
+               .add_node(ShaderNodeBuilder<SeparateXYZNode>("SeparateXYZ")
+                         .set("Vector", make_float3(0.3f, 0.5f, 0.7f)))
+               .add_node(ShaderNodeBuilder<CombineXYZNode>("CombineXYZ"))
+               .add_connection("SeparateXYZ::X", "CombineXYZ::X")
+               .add_connection("SeparateXYZ::Y", "CombineXYZ::Y")
+               .add_connection("SeparateXYZ::Z", "CombineXYZ::Z")
+               .output_color("CombineXYZ::Vector");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Separate/Combine HSV with all constant inputs.
+ */
+TEST(render_graph, constant_fold_separate_combine_hsv)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateHSV::H to constant (0.583333).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateHSV::S to constant (0.571429).");
+       CORRECT_INFO_MESSAGE(log, "Folding SeparateHSV::V to constant (0.7).");
+       CORRECT_INFO_MESSAGE(log, "Folding CombineHSV::Color to constant (0.3, 0.5, 0.7).");
+
+       builder
+               .add_node(ShaderNodeBuilder<SeparateHSVNode>("SeparateHSV")
+                         .set("Color", make_float3(0.3f, 0.5f, 0.7f)))
+               .add_node(ShaderNodeBuilder<CombineHSVNode>("CombineHSV"))
+               .add_connection("SeparateHSV::H", "CombineHSV::H")
+               .add_connection("SeparateHSV::S", "CombineHSV::S")
+               .add_connection("SeparateHSV::V", "CombineHSV::V")
+               .output_color("CombineHSV::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Gamma with all constant inputs.
+ */
+TEST(render_graph, constant_fold_gamma)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Gamma::Color to constant (0.164317, 0.353553, 0.585662).");
+
+       builder
+               .add_node(ShaderNodeBuilder<GammaNode>("Gamma")
+                         .set("Color", make_float3(0.3f, 0.5f, 0.7f))
+                         .set("Gamma", 1.5f))
+               .output_color("Gamma::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: BrightnessContrast with all constant inputs.
+ */
+TEST(render_graph, constant_fold_bright_contrast)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding BrightContrast::Color to constant (0.16, 0.6, 1.04).");
+
+       builder
+               .add_node(ShaderNodeBuilder<BrightContrastNode>("BrightContrast")
+                         .set("Color", make_float3(0.3f, 0.5f, 0.7f))
+                         .set("Bright", 0.1f)
+                         .set("Contrast", 1.2f))
+               .output_color("BrightContrast::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: blackbody with all constant inputs.
+ */
+TEST(render_graph, constant_fold_blackbody)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Blackbody::Color to constant (3.94163, 0.226523, 0).");
+
+       builder
+               .add_node(ShaderNodeBuilder<BlackbodyNode>("Blackbody")
+                         .set("Temperature", 1200.0f))
+               .output_color("Blackbody::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Math with all constant inputs (clamp false).
+ */
+TEST(render_graph, constant_fold_math)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Math::Value to constant (1.6).");
+
+       builder
+               .add_node(ShaderNodeBuilder<MathNode>("Math")
+                         .set(&MathNode::type, NODE_MATH_ADD)
+                         .set(&MathNode::use_clamp, false)
+                         .set("Value1", 0.7f)
+                         .set("Value2", 0.9f))
+               .output_color("Math::Value");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Math with all constant inputs (clamp true).
+ */
+TEST(render_graph, constant_fold_math_clamp)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Math::Value to constant (1).");
+
+       builder
+               .add_node(ShaderNodeBuilder<MathNode>("Math")
+                         .set(&MathNode::type, NODE_MATH_ADD)
+                         .set(&MathNode::use_clamp, true)
+                         .set("Value1", 0.7f)
+                         .set("Value2", 0.9f))
+               .output_color("Math::Value");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Graph for testing partial folds of Math with one constant argument.
+ * Includes 2 tests: constant on each side.
+ */
+static void build_math_partial_test_graph(ShaderGraphBuilder &builder, NodeMath type, float constval)
+{
+       builder
+               .add_attribute("Attribute")
+               /* constant on the left */
+               .add_node(ShaderNodeBuilder<MathNode>("Math_Cx")
+                         .set(&MathNode::type, type)
+                         .set(&MathNode::use_clamp, false)
+                         .set("Value1", constval))
+               .add_connection("Attribute::Fac", "Math_Cx::Value2")
+               /* constant on the right */
+               .add_node(ShaderNodeBuilder<MathNode>("Math_xC")
+                         .set(&MathNode::type, type)
+                         .set(&MathNode::use_clamp, false)
+                         .set("Value2", constval))
+               .add_connection("Attribute::Fac", "Math_xC::Value1")
+               /* output sum */
+               .add_node(ShaderNodeBuilder<MathNode>("Out")
+                         .set(&MathNode::type, NODE_MATH_ADD)
+                         .set(&MathNode::use_clamp, true))
+               .add_connection("Math_Cx::Value", "Out::Value1")
+               .add_connection("Math_xC::Value", "Out::Value2")
+               .output_color("Out::Value");
+}
+
+/*
+ * Tests: partial folding for Math Add with known 0.
+ */
+TEST(render_graph, constant_fold_part_math_add_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X + 0 == 0 + X == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Value to socket Attribute::Fac.");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Value to socket Attribute::Fac.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_math_partial_test_graph(builder, NODE_MATH_ADD, 0.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Math Sub with known 0.
+ */
+TEST(render_graph, constant_fold_part_math_sub_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X - 0 == X */
+       INVALID_INFO_MESSAGE(log, "Folding Math_Cx::");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Value to socket Attribute::Fac.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_math_partial_test_graph(builder, NODE_MATH_SUBTRACT, 0.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Math Mul with known 1.
+ */
+TEST(render_graph, constant_fold_part_math_mul_1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X * 1 == 1 * X == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Value to socket Attribute::Fac.");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Value to socket Attribute::Fac.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_math_partial_test_graph(builder, NODE_MATH_MULTIPLY, 1.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Math Div with known 1.
+ */
+TEST(render_graph, constant_fold_part_math_div_1)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X / 1 == X */
+       INVALID_INFO_MESSAGE(log, "Folding Math_Cx::");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Value to socket Attribute::Fac.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_math_partial_test_graph(builder, NODE_MATH_DIVIDE, 1.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Math Mul with known 0.
+ */
+TEST(render_graph, constant_fold_part_math_mul_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X * 0 == 0 * X == 0 */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Value to constant (0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Value to constant (0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Out::Value to constant (0)");
+       CORRECT_INFO_MESSAGE(log, "Discarding closure EmissionNode.");
+
+       build_math_partial_test_graph(builder, NODE_MATH_MULTIPLY, 0.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Math Div with known 0.
+ */
+TEST(render_graph, constant_fold_part_math_div_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* 0 / X == 0 */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Value to constant (0).");
+       INVALID_INFO_MESSAGE(log, "Folding Math_xC::");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_math_partial_test_graph(builder, NODE_MATH_DIVIDE, 0.0f);
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Vector Math with all constant inputs.
+ */
+TEST(render_graph, constant_fold_vector_math)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding VectorMath::Value to constant (1).");
+       CORRECT_INFO_MESSAGE(log, "Folding VectorMath::Vector to constant (3, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding convert_vector_to_float::value_float to constant (1).");
+       CORRECT_INFO_MESSAGE(log, "Folding Math::Value to constant (2).");
+       CORRECT_INFO_MESSAGE(log, "Folding convert_float_to_color::value_color to constant (2, 2, 2).");
+
+       builder
+               .add_node(ShaderNodeBuilder<VectorMathNode>("VectorMath")
+                         .set(&VectorMathNode::type, NODE_VECTOR_MATH_SUBTRACT)
+                         .set("Vector1", make_float3(1.3f, 0.5f, 0.7f))
+                         .set("Vector2", make_float3(-1.7f, 0.5f, 0.7f)))
+               .add_node(ShaderNodeBuilder<MathNode>("Math")
+                         .set(&MathNode::type, NODE_MATH_ADD))
+               .add_connection("VectorMath::Vector", "Math::Value1")
+               .add_connection("VectorMath::Value", "Math::Value2")
+               .output_color("Math::Value");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Graph for testing partial folds of Vector Math with one constant argument.
+ * Includes 2 tests: constant on each side.
+ */
+static void build_vecmath_partial_test_graph(ShaderGraphBuilder &builder, NodeVectorMath type, float3 constval)
+{
+       builder
+               .add_attribute("Attribute")
+               /* constant on the left */
+               .add_node(ShaderNodeBuilder<VectorMathNode>("Math_Cx")
+                         .set(&VectorMathNode::type, type)
+                         .set("Vector1", constval))
+               .add_connection("Attribute::Vector", "Math_Cx::Vector2")
+               /* constant on the right */
+               .add_node(ShaderNodeBuilder<VectorMathNode>("Math_xC")
+                         .set(&VectorMathNode::type, type)
+                         .set("Vector2", constval))
+               .add_connection("Attribute::Vector", "Math_xC::Vector1")
+               /* output sum */
+               .add_node(ShaderNodeBuilder<VectorMathNode>("Out")
+                         .set(&VectorMathNode::type, NODE_VECTOR_MATH_ADD))
+               .add_connection("Math_Cx::Vector", "Out::Vector1")
+               .add_connection("Math_xC::Vector", "Out::Vector2")
+               .output_color("Out::Vector");
+}
+
+/*
+ * Tests: partial folding for Vector Math Add with known 0.
+ */
+TEST(render_graph, constant_fold_part_vecmath_add_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X + 0 == 0 + X == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Vector to socket Attribute::Vector.");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Vector to socket Attribute::Vector.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_vecmath_partial_test_graph(builder, NODE_VECTOR_MATH_ADD, make_float3(0,0,0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Vector Math Sub with known 0.
+ */
+TEST(render_graph, constant_fold_part_vecmath_sub_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X - 0 == X */
+       INVALID_INFO_MESSAGE(log, "Folding Math_Cx::");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Vector to socket Attribute::Vector.");
+       INVALID_INFO_MESSAGE(log, "Folding Out::");
+
+       build_vecmath_partial_test_graph(builder, NODE_VECTOR_MATH_SUBTRACT, make_float3(0,0,0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Vector Math Dot Product with known 0.
+ */
+TEST(render_graph, constant_fold_part_vecmath_dot_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X * 0 == 0 * X == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Out::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Discarding closure EmissionNode.");
+
+       build_vecmath_partial_test_graph(builder, NODE_VECTOR_MATH_DOT_PRODUCT, make_float3(0,0,0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: partial folding for Vector Math Cross Product with known 0.
+ */
+TEST(render_graph, constant_fold_part_vecmath_cross_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       /* X * 0 == 0 * X == X */
+       CORRECT_INFO_MESSAGE(log, "Folding Math_Cx::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Math_xC::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Folding Out::Vector to constant (0, 0, 0).");
+       CORRECT_INFO_MESSAGE(log, "Discarding closure EmissionNode.");
+
+       build_vecmath_partial_test_graph(builder, NODE_VECTOR_MATH_CROSS_PRODUCT, make_float3(0,0,0));
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Bump with no height input folded to Normal input.
+ */
+TEST(render_graph, constant_fold_bump)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Bump::Normal to socket Geometry1::Normal.");
+
+       builder
+               .add_node(ShaderNodeBuilder<GeometryNode>("Geometry1"))
+               .add_node(ShaderNodeBuilder<BumpNode>("Bump"))
+               .add_connection("Geometry1::Normal", "Bump::Normal")
+               .output_color("Bump::Normal");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests: Bump with no inputs folded to Geometry::Normal.
+ */
+TEST(render_graph, constant_fold_bump_no_input)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Bump::Normal to socket geometry::Normal.");
+
+       builder
+               .add_node(ShaderNodeBuilder<BumpNode>("Bump"))
+               .output_color("Bump::Normal");
+
+       graph.finalize(&scene);
+}
+
+template<class T>
+void init_test_curve(array<T> &buffer, T start, T end, int steps)
+{
+       buffer.resize(steps);
+
+       for (int i = 0; i < steps; i++)
+               buffer[i] = lerp(start, end, float(i)/(steps-1));
+}
+
+/*
+ * Tests:
+ *  - Folding of RGB Curves with all constant inputs.
+ */
+TEST(render_graph, constant_fold_rgb_curves)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Curves::Color to constant (0.275, 0.5, 0.475).");
+
+       array<float3> curve;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 1.0f), make_float3(1.0f, 0.75f, 0.0f), 257);
+
+       builder
+               .add_node(ShaderNodeBuilder<RGBCurvesNode>("Curves")
+                         .set(&CurvesNode::curves, curve)
+                         .set(&CurvesNode::min_x, 0.1f)
+                         .set(&CurvesNode::max_x, 0.9f)
+                         .set("Fac", 0.5f)
+                         .set("Color", make_float3(0.3f, 0.5f, 0.7f)))
+               .output_color("Curves::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of RGB Curves with zero Fac.
+ */
+TEST(render_graph, constant_fold_rgb_curves_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Curves::Color to socket Attribute::Color.");
+
+       array<float3> curve;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 1.0f), make_float3(1.0f, 0.75f, 0.0f), 257);
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<RGBCurvesNode>("Curves")
+                         .set(&CurvesNode::curves, curve)
+                         .set(&CurvesNode::min_x, 0.1f)
+                         .set(&CurvesNode::max_x, 0.9f)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute::Color", "Curves::Color")
+               .output_color("Curves::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Vector Curves with all constant inputs.
+ */
+TEST(render_graph, constant_fold_vector_curves)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Curves::Vector to constant (0.275, 0.5, 0.475).");
+
+       array<float3> curve;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 1.0f), make_float3(1.0f, 0.75f, 0.0f), 257);
+
+       builder
+               .add_node(ShaderNodeBuilder<VectorCurvesNode>("Curves")
+                         .set(&CurvesNode::curves, curve)
+                         .set(&CurvesNode::min_x, 0.1f)
+                         .set(&CurvesNode::max_x, 0.9f)
+                         .set("Fac", 0.5f)
+                         .set("Vector", make_float3(0.3f, 0.5f, 0.7f)))
+               .output_color("Curves::Vector");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Vector Curves with zero Fac.
+ */
+TEST(render_graph, constant_fold_vector_curves_fac_0)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Curves::Vector to socket Attribute::Vector.");
+
+       array<float3> curve;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 1.0f), make_float3(1.0f, 0.75f, 0.0f), 257);
+
+       builder
+               .add_attribute("Attribute")
+               .add_node(ShaderNodeBuilder<VectorCurvesNode>("Curves")
+                         .set(&CurvesNode::curves, curve)
+                         .set(&CurvesNode::min_x, 0.1f)
+                         .set(&CurvesNode::max_x, 0.9f)
+                         .set("Fac", 0.0f))
+               .add_connection("Attribute::Vector", "Curves::Vector")
+               .output_color("Curves::Vector");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Color Ramp with all constant inputs.
+ */
+TEST(render_graph, constant_fold_rgb_ramp)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Ramp::Color to constant (0.14, 0.39, 0.64).");
+       CORRECT_INFO_MESSAGE(log, "Folding Ramp::Alpha to constant (0.89).");
+
+       array<float3> curve;
+       array<float> alpha;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 0.5f), make_float3(0.25f, 0.5f, 0.75f), 9);
+       init_test_curve(alpha, 0.75f, 1.0f, 9);
+
+       builder
+               .add_node(ShaderNodeBuilder<RGBRampNode>("Ramp")
+                         .set(&RGBRampNode::ramp, curve)
+                         .set(&RGBRampNode::ramp_alpha, alpha)
+                         .set(&RGBRampNode::interpolate, true)
+                         .set("Fac", 0.56f))
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_ADD))
+               .add_connection("Ramp::Color", "Mix::Color1")
+               .add_connection("Ramp::Alpha", "Mix::Color2")
+               .output_color("Mix::Color");
+
+       graph.finalize(&scene);
+}
+
+/*
+ * Tests:
+ *  - Folding of Color Ramp with all constant inputs (interpolate false).
+ */
+TEST(render_graph, constant_fold_rgb_ramp_flat)
+{
+       DEFINE_COMMON_VARIABLES(builder, log);
+
+       EXPECT_ANY_MESSAGE(log);
+       CORRECT_INFO_MESSAGE(log, "Folding Ramp::Color to constant (0.125, 0.375, 0.625).");
+       CORRECT_INFO_MESSAGE(log, "Folding Ramp::Alpha to constant (0.875).");
+
+       array<float3> curve;
+       array<float> alpha;
+       init_test_curve(curve, make_float3(0.0f, 0.25f, 0.5f), make_float3(0.25f, 0.5f, 0.75f), 9);
+       init_test_curve(alpha, 0.75f, 1.0f, 9);
+
+       builder
+               .add_node(ShaderNodeBuilder<RGBRampNode>("Ramp")
+                         .set(&RGBRampNode::ramp, curve)
+                         .set(&RGBRampNode::ramp_alpha, alpha)
+                         .set(&RGBRampNode::interpolate, false)
+                         .set("Fac", 0.56f))
+               .add_node(ShaderNodeBuilder<MixNode>("Mix")
+                         .set(&MixNode::type, NODE_MIX_ADD))
+               .add_connection("Ramp::Color", "Mix::Color1")
+               .add_connection("Ramp::Alpha", "Mix::Color2")
+               .output_color("Mix::Color");
 
        graph.finalize(&scene);
 }