From 6b367ed56b1371bcbfacebca2b63f635ed65c1d8 Mon Sep 17 00:00:00 2001 From: kwokcb Date: Mon, 10 Aug 2026 14:49:16 -0400 Subject: [PATCH 1/2] Spec change for `isdefaultversion` when args are ambiguous. --- documents/Specification/MaterialX.Specification.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documents/Specification/MaterialX.Specification.md b/documents/Specification/MaterialX.Specification.md index 29049205c2..e28a1eaf3b 100644 --- a/documents/Specification/MaterialX.Specification.md +++ b/documents/Specification/MaterialX.Specification.md @@ -946,7 +946,7 @@ Attributes for <nodedef> elements: * `inherit` (string, optional): the `name` of a <nodedef> to inherit node definitions from; the output types of this nodedef and the inherited one must match, and the input/output definitions of this nodedef will be applied on top of those in the inherited-from one. * `nodegroup` (string, optional): an optional group to which this node declaration belongs. Standard MaterialX nodes have `nodegroup` values matching the titles of the section headings in which they are described, e.g. "texture2d", "procedural", "geometric", "application", "math", "adjustment", "compositing", "conditional", "channel", "convolution", or "organization". * `version` (string, optional): a version string for this nodedef, allowing usage of a node to reference a specific version of a node. Version strings should be of the format "_major_[._minor_]", i.e. one or two integer numbers separated by a dot (the minor version is assumed to be "0" if not provided). If there are multiple nodedefs for the same `node` and `target` with the same combination of input and output types, they must each specify a `version`. -* `isdefaultversion` (boolean, optional): If true, then this nodedef should be used for node instances which do not request a specific version. Specifying `isdefaultversion` "true" is only required if there are multiple nodedefs for a node declaring a `version`, and it is not permissible for multiple nodedefs for the same `node` and `target` with the same combination of input and output types to set `isdefaultversion` "true". Defaults to "false". +* `isdefaultversion` (boolean, optional): If true, then this nodedef should be used for node instances that do not request a specific version when the declaration is ambiguous. Ambiguity includes cases where a node instance is declared with insufficient inputs to determine a unique nodedef signature, or where multiple nodedef versions are available and no explicit version is specified on the node instance. In these cases, the nodedef with `isdefaultversion` "true" indicates which definition is instantiated. It is not permissible for multiple nodedefs for the same `node` and `target` with the same combination of input and output types to set `isdefaultversion` "true". Defaults to "false". * `target` (stringarray, optional): the set of targets to which this nodedef is restricted. By default, a nodedef is considered universal, not restricted to any specific targets, but it is possible that certain targets may have different parameter names or usage for the same node. * `uiname` (string, optional): an alternative "node" value for this nodedef to be displayed in the UI. If `uiname` is not provided, then `node` is the presumed UI node value for the nodedef. This is most useful when the <nodedef> defines a namespace, so the user doesn't need to see a full namespaced path for the node. * `internalgeomprops` (stringarray, optional): a list of MaterialX geometric properties (e.g. "position", "normal", "texcoord", etc. or any name defined by a <geompropdef> element) that the node expects to be able to access internally. This metadata hint allows code generators to ensure this data is available and can be used for error checking. `Internalgeomprops` is most useful for nodes whose implementation is defined by external code; it is not necessary for nodegraph-defined nodes, as the list of geometric properties accessed can be determined by examining the nodegraph. From 03a1ba18b1789759dffcdf43914a700b9f722cbc Mon Sep 17 00:00:00 2001 From: kwokcb Date: Tue, 11 Aug 2026 12:11:55 -0400 Subject: [PATCH 2/2] Minor logic change to check if there is a "isdefaultversion" set when checking exact or rough matches. Previously it would always return the first found from either exact or rough matches. This is additive and will not affect any existing logic since no library nodes set this attribute for non-versioned nodedefs. Add unit test to new and modifying existing definition behaviour. --- source/MaterialXCore/Node.cpp | 28 ++++++++- source/MaterialXTest/MaterialXCore/Node.cpp | 64 +++++++++++++++++++++ 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/source/MaterialXCore/Node.cpp b/source/MaterialXCore/Node.cpp index f5923f337c..a9f2206324 100644 --- a/source/MaterialXCore/Node.cpp +++ b/source/MaterialXCore/Node.cpp @@ -87,6 +87,7 @@ NodeDefPtr Node::getNodeDef(const string& target, bool allowRoughMatch) const } vector nodeDefs = getDocument()->getMatchingNodeDefs(getQualifiedName(getCategory())); vector secondary = getDocument()->getMatchingNodeDefs(getCategory()); + vector exactMatches; vector roughMatches; nodeDefs.insert(nodeDefs.end(), secondary.begin(), secondary.end()); for (NodeDefPtr nodeDef : nodeDefs) @@ -105,10 +106,35 @@ NodeDefPtr Node::getNodeDef(const string& target, bool allowRoughMatch) const } continue; } - return nodeDef; + exactMatches.push_back(nodeDef); + } + + if (!exactMatches.empty()) + { + if (getVersionString().empty()) + { + for (NodeDefPtr nodeDef : exactMatches) + { + if (nodeDef->getDefaultVersion()) + { + return nodeDef; + } + } + } + return exactMatches[0]; } if (!roughMatches.empty()) { + if (getVersionString().empty()) + { + for (NodeDefPtr nodeDef : roughMatches) + { + if (nodeDef->getDefaultVersion()) + { + return nodeDef; + } + } + } return roughMatches[0]; } return NodeDefPtr(); diff --git a/source/MaterialXTest/MaterialXCore/Node.cpp b/source/MaterialXTest/MaterialXCore/Node.cpp index c43156a4ae..c34adf23d0 100644 --- a/source/MaterialXTest/MaterialXCore/Node.cpp +++ b/source/MaterialXTest/MaterialXCore/Node.cpp @@ -334,6 +334,70 @@ TEST_CASE("Inheritance", "[nodedef]") nodedefSpecularInput->getAttribute(mx::ValueElement::VALUE_ATTRIBUTE)); } +TEST_CASE("Default Instance", "[nodedef]") +{ + mx::FileSearchPath searchPath = mx::getDefaultDataSearchPath(); + mx::DocumentPtr doc = mx::createDocument(); + mx::loadLibraries({ "libraries" }, searchPath, doc); + REQUIRE(doc->validate()); + + // When a node instance has no explicit version and multiple exact matches + // are available, prefer a single nodedef marked as the default version. + mx::NodeDefPtr ambiguousDefV1 = doc->addNodeDef("ND_ambiguous_a", "float", "ambiguous"); + ambiguousDefV1->setNodeGroup(mx::NodeDef::PROCEDURAL_NODE_GROUP); + ambiguousDefV1->setVersionString("1.0"); + ambiguousDefV1->addInput("value", "float"); + mx::NodePtr ambiguousNodeV1 = doc->addNode("ambiguous", mx::EMPTY_STRING, "float"); + ambiguousNodeV1->setVersionString("1.0"); + REQUIRE(ambiguousNodeV1->getNodeDef() == ambiguousDefV1); + + mx::NodeDefPtr ambiguousDefV2 = doc->addNodeDef("ND_ambiguous_b", "float", "ambiguous"); + ambiguousDefV2->setNodeGroup(mx::NodeDef::PROCEDURAL_NODE_GROUP); + ambiguousDefV2->setDefaultVersion(true); + ambiguousDefV2->setVersionString("2.0"); + ambiguousDefV2->addInput("value", "float"); + + mx::NodePtr ambiguousNodeV2 = doc->addNode("ambiguous", mx::EMPTY_STRING, "float"); + ambiguousNodeV2->addInput("value", "float"); + REQUIRE(ambiguousNodeV2->getNodeDef() == ambiguousDefV2); + + // Modify make second nodedef in the list the default version. + // instead of being order dependent resulting in choosing + // the first one found in Node::getNodeDef(). + mx::NodePtr normalmapNode = doc->addNode("normalmap", mx::EMPTY_STRING, "vector3"); + normalmapNode->addInputsFromNodeDef(); + mx::InputPtr normalmapNodeScaleInput = normalmapNode->getInput("scale"); + const std::string previousScaleType = normalmapNodeScaleInput->getType(); + + std::vector nodeDefs = doc->getMatchingNodeDefs(normalmapNode->getQualifiedName(normalmapNode->getCategory())); + std::string defaultScaleType = mx::EMPTY_STRING; + bool isFirstNodeDef = true; + for (auto nodeDef : nodeDefs) + { + if (isFirstNodeDef) + { + nodeDef->setDefaultVersion(false); + isFirstNodeDef = false; + continue; + } + + nodeDef->setDefaultVersion(true); + mx::InputPtr scaleInput = nodeDef->getInput("scale"); + if (defaultScaleType.empty() && scaleInput) + { + defaultScaleType = scaleInput->getType(); + } + } + + // Check new logic and compare against definition returned with a default being set + mx::NodePtr normalmapNodeDefault = doc->addNode("normalmap", mx::EMPTY_STRING, "vector3"); + normalmapNodeDefault->addInputsFromNodeDef(); + mx::InputPtr normalmapNodeDefaultScaleInput = normalmapNodeDefault->getInput("scale"); + REQUIRE(normalmapNodeDefaultScaleInput->getType() == defaultScaleType); + + REQUIRE(defaultScaleType != previousScaleType); +} + TEST_CASE("Topological sort", "[nodegraph]") { // Create a document.